This commit is contained in:
NotAShelf 2026-01-27 09:09:09 +00:00
parent 7701e6026b
commit bbc59631b6
54 changed files with 23 additions and 219186 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,740 +0,0 @@
// Polyfill for requestIdleCallback for Safari and unsupported browsers
if (typeof window.requestIdleCallback === "undefined") {
window.requestIdleCallback = function (cb) {
const start = Date.now();
const idlePeriod = 50;
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, idlePeriod - (Date.now() - start));
},
});
}, 1);
};
window.cancelIdleCallback = function (id) {
clearTimeout(id);
};
}
// Create mobile elements if they don't exist
function createMobileElements() {
// Create mobile sidebar FAB
const mobileFab = document.createElement("button");
mobileFab.className = "mobile-sidebar-fab";
mobileFab.setAttribute("aria-label", "Toggle sidebar menu");
mobileFab.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
`;
// Only show FAB on mobile (max-width: 800px)
function updateFabVisibility() {
if (window.innerWidth > 800) {
if (mobileFab.parentNode) mobileFab.parentNode.removeChild(mobileFab);
} else {
if (!document.body.contains(mobileFab)) {
document.body.appendChild(mobileFab);
}
mobileFab.style.display = "flex";
}
}
updateFabVisibility();
window.addEventListener("resize", updateFabVisibility);
// Create mobile sidebar container
const mobileContainer = document.createElement("div");
mobileContainer.className = "mobile-sidebar-container";
mobileContainer.innerHTML = `
<div class="mobile-sidebar-handle">
<div class="mobile-sidebar-dragger"></div>
</div>
<div class="mobile-sidebar-content">
<!-- Sidebar content will be cloned here -->
</div>
`;
// Create mobile search popup
const mobileSearchPopup = document.createElement("div");
mobileSearchPopup.id = "mobile-search-popup";
mobileSearchPopup.className = "mobile-search-popup";
mobileSearchPopup.innerHTML = `
<div class="mobile-search-container">
<div class="mobile-search-header">
<input type="text" id="mobile-search-input" placeholder="Search..." />
<button id="close-mobile-search" class="close-mobile-search" aria-label="Close search">&times;</button>
</div>
<div id="mobile-search-results" class="mobile-search-results"></div>
</div>
`;
// Insert at end of body so it is not affected by .container flex or stacking context
document.body.appendChild(mobileContainer);
document.body.appendChild(mobileSearchPopup);
// Immediately populate mobile sidebar content if desktop sidebar exists
const desktopSidebar = document.querySelector(".sidebar");
const mobileSidebarContent = mobileContainer.querySelector(
".mobile-sidebar-content",
);
if (desktopSidebar && mobileSidebarContent) {
mobileSidebarContent.innerHTML = desktopSidebar.innerHTML;
}
}
// Initialize collapsible sidebar sections with state persistence
function initCollapsibleSections() {
// Target sections in both desktop and mobile sidebars
const sections = document.querySelectorAll(
".sidebar .sidebar-section, .mobile-sidebar-content .sidebar-section",
);
sections.forEach((section) => {
const sectionId = section.dataset.section;
if (!sectionId) return;
const storageKey = `sidebar-section-${sectionId}`;
const savedState = localStorage.getItem(storageKey);
// Restore saved state (default is open)
if (savedState === "closed") {
section.removeAttribute("open");
}
// Save state on toggle and sync between desktop/mobile
section.addEventListener("toggle", () => {
localStorage.setItem(storageKey, section.open ? "open" : "closed");
// Sync state between desktop and mobile versions
const allWithSameSection = document.querySelectorAll(
`.sidebar-section[data-section="${sectionId}"]`,
);
allWithSameSection.forEach((el) => {
if (el !== section) {
el.open = section.open;
}
});
});
});
}
// Initialize scroll spy
function initScrollSpy() {
const pageToc = document.querySelector(".page-toc");
if (!pageToc) return;
const tocLinks = pageToc.querySelectorAll(".page-toc-list a");
const content = document.querySelector(".content");
if (!tocLinks.length || !content) return;
const headings = Array.from(
content.querySelectorAll("h1[id], h2[id], h3[id]"),
);
if (!headings.length) return;
// Build a map of heading IDs to TOC links for quick lookup
const linkMap = new Map();
tocLinks.forEach((link) => {
const href = link.getAttribute("href");
if (href && href.startsWith("#")) {
linkMap.set(href.slice(1), link);
}
});
let activeLink = null;
// Update active link based on scroll position
function updateActiveLink() {
const threshold = 120; // threshold from the top of the viewport
let currentHeading = null;
// Find the last heading that is at or above the threshold
for (const heading of headings) {
const rect = heading.getBoundingClientRect();
if (rect.top <= threshold) {
currentHeading = heading;
}
}
// If no heading is above threshold, use first heading if it's in view
if (!currentHeading && headings.length > 0) {
const firstRect = headings[0].getBoundingClientRect();
if (firstRect.top < window.innerHeight) {
currentHeading = headings[0];
}
}
const newLink = currentHeading ? linkMap.get(currentHeading.id) : null;
if (newLink !== activeLink) {
if (activeLink) {
activeLink.classList.remove("active");
}
if (newLink) {
newLink.classList.add("active");
}
activeLink = newLink;
}
}
// Scroll event handler
let ticking = false;
function onScroll() {
if (!ticking) {
requestAnimationFrame(() => {
updateActiveLink();
ticking = false;
});
ticking = true;
}
}
window.addEventListener("scroll", onScroll, { passive: true });
// Also update on hash change (direct link navigation)
window.addEventListener("hashchange", () => {
requestAnimationFrame(updateActiveLink);
});
// Set initial active state after a small delay to ensure
// browser has completed any hash-based scrolling
setTimeout(updateActiveLink, 100);
}
document.addEventListener("DOMContentLoaded", function () {
// Apply sidebar state immediately before DOM rendering
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
document.body.classList.add("sidebar-collapsed");
}
if (!document.querySelector(".mobile-sidebar-fab")) {
createMobileElements();
}
// Initialize collapsible sidebar sections
// after mobile elements are created
initCollapsibleSections();
// Initialize scroll spy for page TOC
initScrollSpy();
// Desktop Sidebar Toggle
const sidebarToggle = document.querySelector(".sidebar-toggle");
// On page load, sync the state from `documentElement` to `body`
if (document.documentElement.classList.contains("sidebar-collapsed")) {
document.body.classList.add("sidebar-collapsed");
}
if (sidebarToggle) {
sidebarToggle.addEventListener("click", function () {
// Toggle on both elements for consistency
document.documentElement.classList.toggle("sidebar-collapsed");
document.body.classList.toggle("sidebar-collapsed");
// Use documentElement to check state and save to localStorage
const isCollapsed = document.documentElement.classList.contains(
"sidebar-collapsed",
);
localStorage.setItem("sidebar-collapsed", isCollapsed);
});
}
// Make headings clickable for anchor links
const content = document.querySelector(".content");
if (content) {
const headings = content.querySelectorAll("h1, h2, h3, h4, h5, h6");
headings.forEach(function (heading) {
// Generate a valid, unique ID for each heading
if (!heading.id) {
let baseId = heading.textContent
.toLowerCase()
.replace(/[^a-z0-9\s-_]/g, "") // remove invalid chars
.replace(/^[^a-z]+/, "") // remove leading non-letters
.replace(/[\s-_]+/g, "-")
.replace(/^-+|-+$/g, "") // trim leading/trailing dashes
.trim();
if (!baseId) {
baseId = "section";
}
let id = baseId;
let counter = 1;
while (document.getElementById(id)) {
id = `${baseId}-${counter++}`;
}
heading.id = id;
}
// Make the entire heading clickable
heading.addEventListener("click", function () {
const id = this.id;
history.pushState(null, null, "#" + id);
// Scroll with offset
const offset = this.getBoundingClientRect().top + window.scrollY - 80;
window.scrollTo({
top: offset,
behavior: "smooth",
});
});
});
}
// Process footnotes
if (content) {
const footnoteContainer = document.querySelector(".footnotes-container");
// Find all footnote references and create a footnotes section
const footnoteRefs = content.querySelectorAll('a[href^="#fn"]');
if (footnoteRefs.length > 0) {
const footnotesDiv = document.createElement("div");
footnotesDiv.className = "footnotes";
const footnotesHeading = document.createElement("h2");
footnotesHeading.textContent = "Footnotes";
footnotesDiv.appendChild(footnotesHeading);
const footnotesList = document.createElement("ol");
footnoteContainer.appendChild(footnotesDiv);
footnotesDiv.appendChild(footnotesList);
// Add footnotes
document.querySelectorAll(".footnote").forEach((footnote) => {
const id = footnote.id;
const content = footnote.innerHTML;
const li = document.createElement("li");
li.id = id;
li.innerHTML = content;
// Add backlink
const backlink = document.createElement("a");
backlink.href = "#fnref:" + id.replace("fn:", "");
backlink.className = "footnote-backlink";
backlink.textContent = "↩";
li.appendChild(backlink);
footnotesList.appendChild(li);
});
}
}
// Copy link functionality
document.querySelectorAll(".copy-link").forEach(function (copyLink) {
copyLink.addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
// Get option ID from parent element
const option = copyLink.closest(".option");
const optionId = option.id;
// Create URL with hash
const url = new URL(window.location.href);
url.hash = optionId;
// Copy to clipboard
navigator.clipboard
.writeText(url.toString())
.then(function () {
// Show feedback
const feedback = copyLink.nextElementSibling;
feedback.style.display = "inline";
// Hide after 2 seconds
setTimeout(function () {
feedback.style.display = "none";
}, 2000);
})
.catch(function (err) {
console.error("Could not copy link: ", err);
});
});
});
// Handle initial hash navigation
function scrollToElement(element) {
if (element) {
const offset = element.getBoundingClientRect().top + window.scrollY - 80;
window.scrollTo({
top: offset,
behavior: "smooth",
});
}
}
if (window.location.hash) {
const targetElement = document.querySelector(window.location.hash);
if (targetElement) {
setTimeout(() => scrollToElement(targetElement), 0);
// Add highlight class for options page
if (targetElement.classList.contains("option")) {
targetElement.classList.add("highlight");
}
}
}
// Mobile Sidebar Functionality
const mobileSidebarContainer = document.querySelector(
".mobile-sidebar-container",
);
const mobileSidebarFab = document.querySelector(".mobile-sidebar-fab");
const mobileSidebarHandle = document.querySelector(".mobile-sidebar-handle");
// Always set up FAB if it exists
if (mobileSidebarFab && mobileSidebarContainer) {
const openMobileSidebar = () => {
mobileSidebarContainer.classList.add("active");
mobileSidebarFab.setAttribute("aria-expanded", "true");
mobileSidebarContainer.setAttribute("aria-hidden", "false");
mobileSidebarFab.classList.add("fab-hidden"); // hide FAB when drawer is open
};
const closeMobileSidebar = () => {
mobileSidebarContainer.classList.remove("active");
mobileSidebarFab.setAttribute("aria-expanded", "false");
mobileSidebarContainer.setAttribute("aria-hidden", "true");
mobileSidebarFab.classList.remove("fab-hidden"); // Show FAB when drawer is closed
};
mobileSidebarFab.addEventListener("click", (e) => {
e.stopPropagation();
if (mobileSidebarContainer.classList.contains("active")) {
closeMobileSidebar();
} else {
openMobileSidebar();
}
});
// Only set up drag functionality if handle exists
if (mobileSidebarHandle) {
// Drag functionality
let isDragging = false;
let startY = 0;
let startHeight = 0;
// Cleanup function for drag interruption
function cleanupDrag() {
if (isDragging) {
isDragging = false;
mobileSidebarHandle.style.cursor = "grab";
document.body.style.userSelect = "";
}
}
mobileSidebarHandle.addEventListener("mousedown", (e) => {
isDragging = true;
startY = e.pageY;
startHeight = mobileSidebarContainer.offsetHeight;
mobileSidebarHandle.style.cursor = "grabbing";
document.body.style.userSelect = "none"; // prevent text selection
});
mobileSidebarHandle.addEventListener("touchstart", (e) => {
isDragging = true;
startY = e.touches[0].pageY;
startHeight = mobileSidebarContainer.offsetHeight;
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const deltaY = startY - e.pageY;
const newHeight = startHeight + deltaY;
const vh = window.innerHeight;
const minHeight = vh * 0.15;
const maxHeight = vh * 0.9;
if (newHeight >= minHeight && newHeight <= maxHeight) {
mobileSidebarContainer.style.height = `${newHeight}px`;
}
});
document.addEventListener("touchmove", (e) => {
if (!isDragging) return;
const deltaY = startY - e.touches[0].pageY;
const newHeight = startHeight + deltaY;
const vh = window.innerHeight;
const minHeight = vh * 0.15;
const maxHeight = vh * 0.9;
if (newHeight >= minHeight && newHeight <= maxHeight) {
mobileSidebarContainer.style.height = `${newHeight}px`;
}
});
document.addEventListener("mouseup", cleanupDrag);
document.addEventListener("touchend", cleanupDrag);
window.addEventListener("blur", cleanupDrag);
document.addEventListener("visibilitychange", function () {
if (document.hidden) cleanupDrag();
});
}
// Close on outside click
document.addEventListener("click", (event) => {
if (
mobileSidebarContainer.classList.contains("active") &&
!mobileSidebarContainer.contains(event.target) &&
!mobileSidebarFab.contains(event.target)
) {
closeMobileSidebar();
}
});
// Close on escape key
document.addEventListener("keydown", (event) => {
if (
event.key === "Escape" &&
mobileSidebarContainer.classList.contains("active")
) {
closeMobileSidebar();
}
});
}
// Options filter functionality
const optionsFilter = document.getElementById("options-filter");
if (optionsFilter) {
const optionsContainer = document.querySelector(".options-container");
if (!optionsContainer) return;
// Only inject the style if it doesn't already exist
if (!document.head.querySelector("style[data-options-hidden]")) {
const styleEl = document.createElement("style");
styleEl.setAttribute("data-options-hidden", "");
styleEl.textContent = ".option-hidden{display:none!important}";
document.head.appendChild(styleEl);
}
// Create filter results counter
const filterResults = document.createElement("div");
filterResults.className = "filter-results";
optionsFilter.parentNode.insertBefore(
filterResults,
optionsFilter.nextSibling,
);
// Detect if we're on a mobile device
const isMobile = window.innerWidth < 768 ||
/Mobi|Android/i.test(navigator.userAgent);
// Cache all option elements and their searchable content
const options = Array.from(document.querySelectorAll(".option"));
const totalCount = options.length;
// Store the original order of option elements
const originalOptionOrder = options.slice();
// Pre-process and optimize searchable content
const optionsData = options.map((option) => {
const nameElem = option.querySelector(".option-name");
const descriptionElem = option.querySelector(".option-description");
const id = option.id ? option.id.toLowerCase() : "";
const name = nameElem ? nameElem.textContent.toLowerCase() : "";
const description = descriptionElem
? descriptionElem.textContent.toLowerCase()
: "";
// Extract keywords for faster searching
const keywords = (id + " " + name + " " + description)
.toLowerCase()
.split(/\s+/)
.filter((word) => word.length > 1);
return {
element: option,
id,
name,
description,
keywords,
searchText: (id + " " + name + " " + description).toLowerCase(),
};
});
// Chunk size and rendering variables
const CHUNK_SIZE = isMobile ? 15 : 40;
let pendingRender = null;
let currentChunk = 0;
let itemsToProcess = [];
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// Process options in chunks to prevent UI freezing
function processNextChunk() {
const startIdx = currentChunk * CHUNK_SIZE;
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
if (startIdx < itemsToProcess.length) {
// Process current chunk
for (let i = startIdx; i < endIdx; i++) {
const item = itemsToProcess[i];
if (item.visible) {
item.element.classList.remove("option-hidden");
} else {
item.element.classList.add("option-hidden");
}
}
currentChunk++;
pendingRender = requestAnimationFrame(processNextChunk);
} else {
// Finished processing all chunks
pendingRender = null;
currentChunk = 0;
itemsToProcess = [];
// Update counter at the very end for best performance
if (filterResults.visibleCount !== undefined) {
if (filterResults.visibleCount < totalCount) {
filterResults.textContent =
`Showing ${filterResults.visibleCount} of ${totalCount} options`;
filterResults.style.display = "block";
} else {
filterResults.style.display = "none";
}
}
}
}
function filterOptions() {
const searchTerm = optionsFilter.value.toLowerCase().trim();
if (pendingRender) {
cancelAnimationFrame(pendingRender);
pendingRender = null;
}
currentChunk = 0;
itemsToProcess = [];
if (searchTerm === "") {
// Restore original DOM order when filter is cleared
const fragment = document.createDocumentFragment();
originalOptionOrder.forEach((option) => {
option.classList.remove("option-hidden");
fragment.appendChild(option);
});
optionsContainer.appendChild(fragment);
filterResults.style.display = "none";
return;
}
const searchTerms = searchTerm
.split(/\s+/)
.filter((term) => term.length > 0);
let visibleCount = 0;
const titleMatches = [];
const descMatches = [];
optionsData.forEach((data) => {
let isTitleMatch = false;
let isDescMatch = false;
if (searchTerms.length === 1) {
const term = searchTerms[0];
isTitleMatch = data.name.includes(term);
isDescMatch = !isTitleMatch && data.description.includes(term);
} else {
isTitleMatch = searchTerms.every((term) => data.name.includes(term));
isDescMatch = !isTitleMatch &&
searchTerms.every((term) => data.description.includes(term));
}
if (isTitleMatch) {
titleMatches.push(data);
} else if (isDescMatch) {
descMatches.push(data);
}
});
if (searchTerms.length === 1) {
const term = searchTerms[0];
titleMatches.sort(
(a, b) => a.name.indexOf(term) - b.name.indexOf(term),
);
descMatches.sort(
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
);
}
itemsToProcess = [];
titleMatches.forEach((data) => {
visibleCount++;
itemsToProcess.push({ element: data.element, visible: true });
});
descMatches.forEach((data) => {
visibleCount++;
itemsToProcess.push({ element: data.element, visible: true });
});
optionsData.forEach((data) => {
if (!itemsToProcess.some((item) => item.element === data.element)) {
itemsToProcess.push({ element: data.element, visible: false });
}
});
// Reorder DOM so all title matches, then desc matches, then hidden
const fragment = document.createDocumentFragment();
itemsToProcess.forEach((item) => {
fragment.appendChild(item.element);
});
optionsContainer.appendChild(fragment);
filterResults.visibleCount = visibleCount;
pendingRender = requestAnimationFrame(processNextChunk);
}
// Use different debounce times for desktop vs mobile
const debouncedFilter = debounce(filterOptions, isMobile ? 200 : 100);
// Set up event listeners
optionsFilter.addEventListener("input", debouncedFilter);
optionsFilter.addEventListener("change", filterOptions);
// Allow clearing with Escape key
optionsFilter.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
optionsFilter.value = "";
filterOptions();
}
});
// Handle visibility changes
document.addEventListener("visibilitychange", function () {
if (!document.hidden && optionsFilter.value) {
filterOptions();
}
});
// Initially trigger filter if there's a value
if (optionsFilter.value) {
filterOptions();
}
// Pre-calculate heights for smoother scrolling
if (isMobile && totalCount > 50) {
requestIdleCallback(() => {
const sampleOption = options[0];
if (sampleOption) {
const height = sampleOption.offsetHeight;
if (height > 0) {
options.forEach((opt) => {
opt.style.containIntrinsicSize = `0 ${height}px`;
});
}
}
});
}
}
});

File diff suppressed because one or more lines are too long

View file

@ -1,298 +0,0 @@
const isWordBoundary = (char) =>
/[A-Z]/.test(char) || /[-_\/.]/.test(char) || /\s/.test(char);
const isCaseTransition = (prev, curr) => {
const prevIsUpper = prev.toLowerCase() !== prev;
const currIsUpper = curr.toLowerCase() !== curr;
return (
prevIsUpper && currIsUpper && prev.toLowerCase() !== curr.toLowerCase()
);
};
const findBestSubsequenceMatch = (query, target) => {
const n = query.length;
const m = target.length;
if (n === 0 || m === 0) return null;
const positions = [];
const memo = new Map();
const key = (qIdx, tIdx, gap) => `${qIdx}:${tIdx}:${gap}`;
const findBest = (qIdx, tIdx, currentGap) => {
if (qIdx === n) {
return { done: true, positions: [...positions], gap: currentGap };
}
const memoKey = key(qIdx, tIdx, currentGap);
if (memo.has(memoKey)) {
return memo.get(memoKey);
}
let bestResult = null;
for (let i = tIdx; i < m; i++) {
if (target[i] === query[qIdx]) {
positions.push(i);
const gap = qIdx === 0 ? 0 : i - positions[positions.length - 2] - 1;
const newGap = currentGap + gap;
if (newGap > m) {
positions.pop();
continue;
}
const result = findBest(qIdx + 1, i + 1, newGap);
positions.pop();
if (result && (!bestResult || result.gap < bestResult.gap)) {
bestResult = result;
if (result.gap === 0) break;
}
}
}
memo.set(memoKey, bestResult);
return bestResult;
};
const result = findBest(0, 0, 0);
if (!result) return null;
const consecutive = (() => {
let c = 1;
for (let i = 1; i < result.positions.length; i++) {
if (result.positions[i] === result.positions[i - 1] + 1) {
c++;
}
}
return c;
})();
return {
positions: result.positions,
consecutive,
score: calculateMatchScore(query, target, result.positions, consecutive),
};
};
const calculateMatchScore = (query, target, positions, consecutive) => {
const n = positions.length;
const m = target.length;
if (n === 0) return 0;
let score = 1.0;
const startBonus = (m - positions[0]) / m;
score += startBonus * 0.5;
let gapPenalty = 0;
for (let i = 1; i < n; i++) {
const gap = positions[i] - positions[i - 1] - 1;
if (gap > 0) {
gapPenalty += Math.min(gap / m, 1.0) * 0.3;
}
}
score -= gapPenalty;
const consecutiveBonus = consecutive / n;
score += consecutiveBonus * 0.3;
let boundaryBonus = 0;
for (let i = 0; i < n; i++) {
const char = target[positions[i]];
if (i === 0 || isWordBoundary(char)) {
boundaryBonus += 0.05;
}
if (i > 0) {
const prevChar = target[positions[i - 1]];
if (isCaseTransition(prevChar, char)) {
boundaryBonus += 0.03;
}
}
}
score = Math.min(1.0, score + boundaryBonus);
const lengthPenalty = Math.abs(query.length - n) / Math.max(query.length, m);
score -= lengthPenalty * 0.2;
return Math.max(0, Math.min(1.0, score));
};
const fuzzyMatch = (query, target) => {
const lowerQuery = query.toLowerCase();
const lowerTarget = target.toLowerCase();
if (lowerQuery.length === 0) return null;
if (lowerTarget.length === 0) return null;
if (lowerTarget === lowerQuery) {
return 1.0;
}
if (lowerTarget.includes(lowerQuery)) {
const ratio = lowerQuery.length / lowerTarget.length;
return 0.8 + ratio * 0.2;
}
const match = findBestSubsequenceMatch(lowerQuery, lowerTarget);
if (!match) {
return null;
}
return Math.min(1.0, match.score);
};
self.onmessage = function (e) {
const { messageId, type, data } = e.data;
const respond = (type, data) => {
self.postMessage({ messageId, type, data });
};
const respondError = (error) => {
self.postMessage({
messageId,
type: "error",
error: error.message || String(error),
});
};
try {
if (type === "tokenize") {
const text = typeof data === "string" ? data : "";
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
const tokens = words.filter((word) => word.length > 2);
const uniqueTokens = Array.from(new Set(tokens));
respond("tokens", uniqueTokens);
} else if (type === "search") {
const { query, limit = 10 } = data;
if (!query || typeof query !== "string") {
respond("results", []);
return;
}
const rawQuery = query.toLowerCase();
const text = typeof query === "string" ? query : "";
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
const searchTerms = words.filter((word) => word.length > 2);
let documents = [];
if (typeof data.documents === "string") {
documents = JSON.parse(data.documents);
} else if (Array.isArray(data.documents)) {
documents = data.documents;
} else if (typeof data.transferables === "string") {
documents = JSON.parse(data.transferables);
}
if (!Array.isArray(documents) || documents.length === 0) {
respond("results", []);
return;
}
const useFuzzySearch = rawQuery.length >= 3;
if (searchTerms.length === 0 && rawQuery.length < 3) {
respond("results", []);
return;
}
const pageMatches = new Map();
// Pre-compute lower-case strings for each document
const processedDocs = documents.map((doc, docId) => {
const title = typeof doc.title === "string" ? doc.title : "";
const content = typeof doc.content === "string" ? doc.content : "";
return {
docId,
doc,
lowerTitle: title.toLowerCase(),
lowerContent: content.toLowerCase(),
};
});
// First pass: Score pages with fuzzy matching
processedDocs.forEach(({ docId, doc, lowerTitle, lowerContent }) => {
let match = pageMatches.get(docId);
if (!match) {
match = { doc, pageScore: 0, matchingAnchors: [] };
pageMatches.set(docId, match);
}
if (useFuzzySearch) {
const fuzzyTitleScore = fuzzyMatch(rawQuery, lowerTitle);
if (fuzzyTitleScore !== null) {
match.pageScore += fuzzyTitleScore * 100;
}
const fuzzyContentScore = fuzzyMatch(rawQuery, lowerContent);
if (fuzzyContentScore !== null) {
match.pageScore += fuzzyContentScore * 30;
}
}
// Token-based exact matching
searchTerms.forEach((term) => {
if (lowerTitle.includes(term)) {
match.pageScore += lowerTitle === term ? 20 : 10;
}
if (lowerContent.includes(term)) {
match.pageScore += 2;
}
});
});
// Second pass: Find matching anchors
pageMatches.forEach((match) => {
const doc = match.doc;
if (
!doc.anchors ||
!Array.isArray(doc.anchors) ||
doc.anchors.length === 0
) {
return;
}
doc.anchors.forEach((anchor) => {
if (!anchor || !anchor.text) return;
const anchorText = anchor.text.toLowerCase();
let anchorMatches = false;
if (useFuzzySearch) {
const fuzzyScore = fuzzyMatch(rawQuery, anchorText);
if (fuzzyScore !== null && fuzzyScore >= 0.4) {
anchorMatches = true;
}
}
if (!anchorMatches) {
searchTerms.forEach((term) => {
if (anchorText.includes(term)) {
anchorMatches = true;
}
});
}
if (anchorMatches) {
match.matchingAnchors.push(anchor);
}
});
});
const results = Array.from(pageMatches.values())
.filter((m) => m.pageScore > 5)
.sort((a, b) => b.pageScore - a.pageScore)
.slice(0, limit);
respond("results", results);
}
} catch (error) {
respondError(error);
}
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,153 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Known Issues and Quirks</title>
<script>
// Apply sidebar state immediately to prevent flash
(function () {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
})();
</script>
<link rel="stylesheet" href="assets/style.css" />
<script defer src="assets/main.js"></script>
<script>
window.searchNamespace = window.searchNamespace || {};
window.searchNamespace.rootPath = "";
</script>
<script defer src="assets/search.js"></script>
</head>
<body>
<div class="container">
<header>
<div class="header-left">
<h1 class="site-title">
<a href="index.html">NVF</a>
</h1>
<nav class="header-nav">
<ul>
<li >
<a href="options.html">Options</a>
</li>
<li><a href="search.html">Search</a></li>
</ul>
</nav>
</div>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search..." />
<div id="search-results" class="search-results"></div>
</div>
</header>
<div class="layout">
<div class="sidebar-toggle" aria-label="Toggle sidebar">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
</svg>
</div>
<nav class="sidebar">
<details class="sidebar-section" data-section="docs" open>
<summary>Documents</summary>
<div class="sidebar-section-content">
<ul>
<li><a href="index.html">Introduction</a></li>
<li><a href="configuring.html">Configuring nvf</a></li>
<li><a href="hacking.html">Hacking nvf</a></li>
<li><a href="tips.html">Helpful Tips</a></li>
<li><a href="quirks.html">Known Issues and Quirks</a></li>
<li><a href="release-notes.html">Release Notes</a></li>
<li><a href="search.html">Search</a></li>
</ul>
</div>
</details>
<details class="sidebar-section" data-section="toc" open>
<summary>Contents</summary>
<div class="sidebar-section-content">
<ul class="toc-list">
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
</ul><li><a href="#ch-bugs-suggestions">Bugs &amp; Suggestions</a>
</li></ul></li>
</ul>
</div>
</details>
</nav>
<main class="content"><html><head></head><body><h1 id="ch-known-issues-quirks">Known Issues and Quirks</h1>
<p>At times, certain plugins and modules may refuse to play nicely with your setup,
be it a result of generating Lua from Nix, or the state of packaging. This page,
in turn, will list any known modules or plugins that are known to misbehave, and
possible workarounds that you may apply.</p>
<h2 id="ch-quirks-nodejs">NodeJS</h2>
<h3 id="sec-eslint-plugin-prettier">eslint-plugin-prettier</h3>
<p>When working with NodeJS, which is <em>obviously</em> known for its meticulous
standards, most things are bound to work as expected but some projects, tools
and settings may fool the default configurations of tools provided by <strong>nvf</strong>.</p>
<p>If</p>
<p>If <a href="https://github.com/prettier/eslint-plugin-prettier">eslint-plugin-prettier</a> or similar is included, you might get a situation
where your Eslint configuration diagnoses your formatting according to its own
config (usually <code>.eslintrc.js</code>). The issue there is your formatting is made via
prettierd.</p>
<p>This results in auto-formatting relying on your prettier configuration, while
your Eslint configuration diagnoses formatting "issues" while it's
<a href="https://prettier.io/docs/en/comparison.html">not supposed to</a>. In the end, you get discrepancies between what your editor
does and what it wants.</p>
<p>Solutions are:</p>
<ol>
<li>Don't add a formatting config to Eslint, instead separate Prettier and
Eslint.</li>
<li>PR the repo in question to add an ESLint formatter, and configure <strong>nvf</strong> to
use it.</li>
</ol>
<h2 id="ch-bugs-suggestions">Bugs &amp; Suggestions</h2>
<p>Some quirks are not exactly quirks, but bugs in the module system. If you notice
any issues with <strong>nvf</strong>, or this documentation, then please consider reporting
them over at the <a href="https://github.com/notashelf/nvf/issues">issue tracker</a>. Issues tab, in addition to the
<a href="https://github.com/notashelf/nvf/discussions">discussions tab</a> is a good place as any to request new features.</p>
<p>You may also consider submitting bug fixes, feature additions and upstreamed
changes that you think are critical over at the <a href="https://github.com/notashelf/nvf/pulls">pull requests tab</a>.</p>
</body></html></main>
</div>
<aside class="page-toc">
<nav class="page-toc-nav">
<h3>On this page</h3>
<ul class="page-toc-list">
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
</ul><li><a href="#ch-bugs-suggestions">Bugs &amp; Suggestions</a>
</li></ul></li>
</ul>
</nav>
</aside>
<footer>
<p>Generated with ndg</p>
</footer>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,110 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NVF - Search</title>
<script>
// Apply sidebar state immediately to prevent flash
(function () {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
})();
</script>
<link rel="stylesheet" href="assets/style.css" />
<script defer src="assets/main.js"></script>
<script>
window.searchNamespace = window.searchNamespace || {};
window.searchNamespace.rootPath = "";
</script>
<script defer src="assets/search.js"></script>
</head>
<body>
<div class="container">
<header>
<div class="header-left">
<h1 class="site-title">
<a href="index.html">NVF</a>
</h1>
</div>
<nav class="header-nav">
<ul>
<li >
<a href="options.html">Options</a>
</li>
<li><a href="search.html">Search</a></li>
</ul>
</nav>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search..." />
<div id="search-results" class="search-results"></div>
</div>
</header>
<div class="layout">
<div class="sidebar-toggle" aria-label="Toggle sidebar">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
</svg>
</div>
<nav id="sidebar" class="sidebar">
<div class="docs-nav">
<h2>Documents</h2>
<ul>
<li><a href="index.html">Introduction</a></li>
<li><a href="configuring.html">Configuring nvf</a></li>
<li><a href="hacking.html">Hacking nvf</a></li>
<li><a href="tips.html">Helpful Tips</a></li>
<li><a href="quirks.html">Known Issues and Quirks</a></li>
<li><a href="release-notes.html">Release Notes</a></li>
<li><a href="search.html">Search</a></li>
</ul>
</div>
<div class="toc">
<h2>Contents</h2>
<ul class="toc-list">
</ul>
</div>
</nav>
<main class="content">
<h1>Search</h1>
<div class="search-page">
<div class="search-form">
<input
type="text"
id="search-page-input"
placeholder="Search..."
autofocus
/>
</div>
<div id="search-page-results" class="search-page-results"></div>
</div>
<div class="footnotes-container">
<!-- Footnotes will be appended here -->
</div>
</main>
</div>
<footer>
<p>Generated with ndg</p>
</footer>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,740 +0,0 @@
// Polyfill for requestIdleCallback for Safari and unsupported browsers
if (typeof window.requestIdleCallback === "undefined") {
window.requestIdleCallback = function (cb) {
const start = Date.now();
const idlePeriod = 50;
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, idlePeriod - (Date.now() - start));
},
});
}, 1);
};
window.cancelIdleCallback = function (id) {
clearTimeout(id);
};
}
// Create mobile elements if they don't exist
function createMobileElements() {
// Create mobile sidebar FAB
const mobileFab = document.createElement("button");
mobileFab.className = "mobile-sidebar-fab";
mobileFab.setAttribute("aria-label", "Toggle sidebar menu");
mobileFab.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
`;
// Only show FAB on mobile (max-width: 800px)
function updateFabVisibility() {
if (window.innerWidth > 800) {
if (mobileFab.parentNode) mobileFab.parentNode.removeChild(mobileFab);
} else {
if (!document.body.contains(mobileFab)) {
document.body.appendChild(mobileFab);
}
mobileFab.style.display = "flex";
}
}
updateFabVisibility();
window.addEventListener("resize", updateFabVisibility);
// Create mobile sidebar container
const mobileContainer = document.createElement("div");
mobileContainer.className = "mobile-sidebar-container";
mobileContainer.innerHTML = `
<div class="mobile-sidebar-handle">
<div class="mobile-sidebar-dragger"></div>
</div>
<div class="mobile-sidebar-content">
<!-- Sidebar content will be cloned here -->
</div>
`;
// Create mobile search popup
const mobileSearchPopup = document.createElement("div");
mobileSearchPopup.id = "mobile-search-popup";
mobileSearchPopup.className = "mobile-search-popup";
mobileSearchPopup.innerHTML = `
<div class="mobile-search-container">
<div class="mobile-search-header">
<input type="text" id="mobile-search-input" placeholder="Search..." />
<button id="close-mobile-search" class="close-mobile-search" aria-label="Close search">&times;</button>
</div>
<div id="mobile-search-results" class="mobile-search-results"></div>
</div>
`;
// Insert at end of body so it is not affected by .container flex or stacking context
document.body.appendChild(mobileContainer);
document.body.appendChild(mobileSearchPopup);
// Immediately populate mobile sidebar content if desktop sidebar exists
const desktopSidebar = document.querySelector(".sidebar");
const mobileSidebarContent = mobileContainer.querySelector(
".mobile-sidebar-content",
);
if (desktopSidebar && mobileSidebarContent) {
mobileSidebarContent.innerHTML = desktopSidebar.innerHTML;
}
}
// Initialize collapsible sidebar sections with state persistence
function initCollapsibleSections() {
// Target sections in both desktop and mobile sidebars
const sections = document.querySelectorAll(
".sidebar .sidebar-section, .mobile-sidebar-content .sidebar-section",
);
sections.forEach((section) => {
const sectionId = section.dataset.section;
if (!sectionId) return;
const storageKey = `sidebar-section-${sectionId}`;
const savedState = localStorage.getItem(storageKey);
// Restore saved state (default is open)
if (savedState === "closed") {
section.removeAttribute("open");
}
// Save state on toggle and sync between desktop/mobile
section.addEventListener("toggle", () => {
localStorage.setItem(storageKey, section.open ? "open" : "closed");
// Sync state between desktop and mobile versions
const allWithSameSection = document.querySelectorAll(
`.sidebar-section[data-section="${sectionId}"]`,
);
allWithSameSection.forEach((el) => {
if (el !== section) {
el.open = section.open;
}
});
});
});
}
// Initialize scroll spy
function initScrollSpy() {
const pageToc = document.querySelector(".page-toc");
if (!pageToc) return;
const tocLinks = pageToc.querySelectorAll(".page-toc-list a");
const content = document.querySelector(".content");
if (!tocLinks.length || !content) return;
const headings = Array.from(
content.querySelectorAll("h1[id], h2[id], h3[id]"),
);
if (!headings.length) return;
// Build a map of heading IDs to TOC links for quick lookup
const linkMap = new Map();
tocLinks.forEach((link) => {
const href = link.getAttribute("href");
if (href && href.startsWith("#")) {
linkMap.set(href.slice(1), link);
}
});
let activeLink = null;
// Update active link based on scroll position
function updateActiveLink() {
const threshold = 120; // threshold from the top of the viewport
let currentHeading = null;
// Find the last heading that is at or above the threshold
for (const heading of headings) {
const rect = heading.getBoundingClientRect();
if (rect.top <= threshold) {
currentHeading = heading;
}
}
// If no heading is above threshold, use first heading if it's in view
if (!currentHeading && headings.length > 0) {
const firstRect = headings[0].getBoundingClientRect();
if (firstRect.top < window.innerHeight) {
currentHeading = headings[0];
}
}
const newLink = currentHeading ? linkMap.get(currentHeading.id) : null;
if (newLink !== activeLink) {
if (activeLink) {
activeLink.classList.remove("active");
}
if (newLink) {
newLink.classList.add("active");
}
activeLink = newLink;
}
}
// Scroll event handler
let ticking = false;
function onScroll() {
if (!ticking) {
requestAnimationFrame(() => {
updateActiveLink();
ticking = false;
});
ticking = true;
}
}
window.addEventListener("scroll", onScroll, { passive: true });
// Also update on hash change (direct link navigation)
window.addEventListener("hashchange", () => {
requestAnimationFrame(updateActiveLink);
});
// Set initial active state after a small delay to ensure
// browser has completed any hash-based scrolling
setTimeout(updateActiveLink, 100);
}
document.addEventListener("DOMContentLoaded", function () {
// Apply sidebar state immediately before DOM rendering
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
document.body.classList.add("sidebar-collapsed");
}
if (!document.querySelector(".mobile-sidebar-fab")) {
createMobileElements();
}
// Initialize collapsible sidebar sections
// after mobile elements are created
initCollapsibleSections();
// Initialize scroll spy for page TOC
initScrollSpy();
// Desktop Sidebar Toggle
const sidebarToggle = document.querySelector(".sidebar-toggle");
// On page load, sync the state from `documentElement` to `body`
if (document.documentElement.classList.contains("sidebar-collapsed")) {
document.body.classList.add("sidebar-collapsed");
}
if (sidebarToggle) {
sidebarToggle.addEventListener("click", function () {
// Toggle on both elements for consistency
document.documentElement.classList.toggle("sidebar-collapsed");
document.body.classList.toggle("sidebar-collapsed");
// Use documentElement to check state and save to localStorage
const isCollapsed = document.documentElement.classList.contains(
"sidebar-collapsed",
);
localStorage.setItem("sidebar-collapsed", isCollapsed);
});
}
// Make headings clickable for anchor links
const content = document.querySelector(".content");
if (content) {
const headings = content.querySelectorAll("h1, h2, h3, h4, h5, h6");
headings.forEach(function (heading) {
// Generate a valid, unique ID for each heading
if (!heading.id) {
let baseId = heading.textContent
.toLowerCase()
.replace(/[^a-z0-9\s-_]/g, "") // remove invalid chars
.replace(/^[^a-z]+/, "") // remove leading non-letters
.replace(/[\s-_]+/g, "-")
.replace(/^-+|-+$/g, "") // trim leading/trailing dashes
.trim();
if (!baseId) {
baseId = "section";
}
let id = baseId;
let counter = 1;
while (document.getElementById(id)) {
id = `${baseId}-${counter++}`;
}
heading.id = id;
}
// Make the entire heading clickable
heading.addEventListener("click", function () {
const id = this.id;
history.pushState(null, null, "#" + id);
// Scroll with offset
const offset = this.getBoundingClientRect().top + window.scrollY - 80;
window.scrollTo({
top: offset,
behavior: "smooth",
});
});
});
}
// Process footnotes
if (content) {
const footnoteContainer = document.querySelector(".footnotes-container");
// Find all footnote references and create a footnotes section
const footnoteRefs = content.querySelectorAll('a[href^="#fn"]');
if (footnoteRefs.length > 0) {
const footnotesDiv = document.createElement("div");
footnotesDiv.className = "footnotes";
const footnotesHeading = document.createElement("h2");
footnotesHeading.textContent = "Footnotes";
footnotesDiv.appendChild(footnotesHeading);
const footnotesList = document.createElement("ol");
footnoteContainer.appendChild(footnotesDiv);
footnotesDiv.appendChild(footnotesList);
// Add footnotes
document.querySelectorAll(".footnote").forEach((footnote) => {
const id = footnote.id;
const content = footnote.innerHTML;
const li = document.createElement("li");
li.id = id;
li.innerHTML = content;
// Add backlink
const backlink = document.createElement("a");
backlink.href = "#fnref:" + id.replace("fn:", "");
backlink.className = "footnote-backlink";
backlink.textContent = "↩";
li.appendChild(backlink);
footnotesList.appendChild(li);
});
}
}
// Copy link functionality
document.querySelectorAll(".copy-link").forEach(function (copyLink) {
copyLink.addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
// Get option ID from parent element
const option = copyLink.closest(".option");
const optionId = option.id;
// Create URL with hash
const url = new URL(window.location.href);
url.hash = optionId;
// Copy to clipboard
navigator.clipboard
.writeText(url.toString())
.then(function () {
// Show feedback
const feedback = copyLink.nextElementSibling;
feedback.style.display = "inline";
// Hide after 2 seconds
setTimeout(function () {
feedback.style.display = "none";
}, 2000);
})
.catch(function (err) {
console.error("Could not copy link: ", err);
});
});
});
// Handle initial hash navigation
function scrollToElement(element) {
if (element) {
const offset = element.getBoundingClientRect().top + window.scrollY - 80;
window.scrollTo({
top: offset,
behavior: "smooth",
});
}
}
if (window.location.hash) {
const targetElement = document.querySelector(window.location.hash);
if (targetElement) {
setTimeout(() => scrollToElement(targetElement), 0);
// Add highlight class for options page
if (targetElement.classList.contains("option")) {
targetElement.classList.add("highlight");
}
}
}
// Mobile Sidebar Functionality
const mobileSidebarContainer = document.querySelector(
".mobile-sidebar-container",
);
const mobileSidebarFab = document.querySelector(".mobile-sidebar-fab");
const mobileSidebarHandle = document.querySelector(".mobile-sidebar-handle");
// Always set up FAB if it exists
if (mobileSidebarFab && mobileSidebarContainer) {
const openMobileSidebar = () => {
mobileSidebarContainer.classList.add("active");
mobileSidebarFab.setAttribute("aria-expanded", "true");
mobileSidebarContainer.setAttribute("aria-hidden", "false");
mobileSidebarFab.classList.add("fab-hidden"); // hide FAB when drawer is open
};
const closeMobileSidebar = () => {
mobileSidebarContainer.classList.remove("active");
mobileSidebarFab.setAttribute("aria-expanded", "false");
mobileSidebarContainer.setAttribute("aria-hidden", "true");
mobileSidebarFab.classList.remove("fab-hidden"); // Show FAB when drawer is closed
};
mobileSidebarFab.addEventListener("click", (e) => {
e.stopPropagation();
if (mobileSidebarContainer.classList.contains("active")) {
closeMobileSidebar();
} else {
openMobileSidebar();
}
});
// Only set up drag functionality if handle exists
if (mobileSidebarHandle) {
// Drag functionality
let isDragging = false;
let startY = 0;
let startHeight = 0;
// Cleanup function for drag interruption
function cleanupDrag() {
if (isDragging) {
isDragging = false;
mobileSidebarHandle.style.cursor = "grab";
document.body.style.userSelect = "";
}
}
mobileSidebarHandle.addEventListener("mousedown", (e) => {
isDragging = true;
startY = e.pageY;
startHeight = mobileSidebarContainer.offsetHeight;
mobileSidebarHandle.style.cursor = "grabbing";
document.body.style.userSelect = "none"; // prevent text selection
});
mobileSidebarHandle.addEventListener("touchstart", (e) => {
isDragging = true;
startY = e.touches[0].pageY;
startHeight = mobileSidebarContainer.offsetHeight;
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const deltaY = startY - e.pageY;
const newHeight = startHeight + deltaY;
const vh = window.innerHeight;
const minHeight = vh * 0.15;
const maxHeight = vh * 0.9;
if (newHeight >= minHeight && newHeight <= maxHeight) {
mobileSidebarContainer.style.height = `${newHeight}px`;
}
});
document.addEventListener("touchmove", (e) => {
if (!isDragging) return;
const deltaY = startY - e.touches[0].pageY;
const newHeight = startHeight + deltaY;
const vh = window.innerHeight;
const minHeight = vh * 0.15;
const maxHeight = vh * 0.9;
if (newHeight >= minHeight && newHeight <= maxHeight) {
mobileSidebarContainer.style.height = `${newHeight}px`;
}
});
document.addEventListener("mouseup", cleanupDrag);
document.addEventListener("touchend", cleanupDrag);
window.addEventListener("blur", cleanupDrag);
document.addEventListener("visibilitychange", function () {
if (document.hidden) cleanupDrag();
});
}
// Close on outside click
document.addEventListener("click", (event) => {
if (
mobileSidebarContainer.classList.contains("active") &&
!mobileSidebarContainer.contains(event.target) &&
!mobileSidebarFab.contains(event.target)
) {
closeMobileSidebar();
}
});
// Close on escape key
document.addEventListener("keydown", (event) => {
if (
event.key === "Escape" &&
mobileSidebarContainer.classList.contains("active")
) {
closeMobileSidebar();
}
});
}
// Options filter functionality
const optionsFilter = document.getElementById("options-filter");
if (optionsFilter) {
const optionsContainer = document.querySelector(".options-container");
if (!optionsContainer) return;
// Only inject the style if it doesn't already exist
if (!document.head.querySelector("style[data-options-hidden]")) {
const styleEl = document.createElement("style");
styleEl.setAttribute("data-options-hidden", "");
styleEl.textContent = ".option-hidden{display:none!important}";
document.head.appendChild(styleEl);
}
// Create filter results counter
const filterResults = document.createElement("div");
filterResults.className = "filter-results";
optionsFilter.parentNode.insertBefore(
filterResults,
optionsFilter.nextSibling,
);
// Detect if we're on a mobile device
const isMobile = window.innerWidth < 768 ||
/Mobi|Android/i.test(navigator.userAgent);
// Cache all option elements and their searchable content
const options = Array.from(document.querySelectorAll(".option"));
const totalCount = options.length;
// Store the original order of option elements
const originalOptionOrder = options.slice();
// Pre-process and optimize searchable content
const optionsData = options.map((option) => {
const nameElem = option.querySelector(".option-name");
const descriptionElem = option.querySelector(".option-description");
const id = option.id ? option.id.toLowerCase() : "";
const name = nameElem ? nameElem.textContent.toLowerCase() : "";
const description = descriptionElem
? descriptionElem.textContent.toLowerCase()
: "";
// Extract keywords for faster searching
const keywords = (id + " " + name + " " + description)
.toLowerCase()
.split(/\s+/)
.filter((word) => word.length > 1);
return {
element: option,
id,
name,
description,
keywords,
searchText: (id + " " + name + " " + description).toLowerCase(),
};
});
// Chunk size and rendering variables
const CHUNK_SIZE = isMobile ? 15 : 40;
let pendingRender = null;
let currentChunk = 0;
let itemsToProcess = [];
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// Process options in chunks to prevent UI freezing
function processNextChunk() {
const startIdx = currentChunk * CHUNK_SIZE;
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
if (startIdx < itemsToProcess.length) {
// Process current chunk
for (let i = startIdx; i < endIdx; i++) {
const item = itemsToProcess[i];
if (item.visible) {
item.element.classList.remove("option-hidden");
} else {
item.element.classList.add("option-hidden");
}
}
currentChunk++;
pendingRender = requestAnimationFrame(processNextChunk);
} else {
// Finished processing all chunks
pendingRender = null;
currentChunk = 0;
itemsToProcess = [];
// Update counter at the very end for best performance
if (filterResults.visibleCount !== undefined) {
if (filterResults.visibleCount < totalCount) {
filterResults.textContent =
`Showing ${filterResults.visibleCount} of ${totalCount} options`;
filterResults.style.display = "block";
} else {
filterResults.style.display = "none";
}
}
}
}
function filterOptions() {
const searchTerm = optionsFilter.value.toLowerCase().trim();
if (pendingRender) {
cancelAnimationFrame(pendingRender);
pendingRender = null;
}
currentChunk = 0;
itemsToProcess = [];
if (searchTerm === "") {
// Restore original DOM order when filter is cleared
const fragment = document.createDocumentFragment();
originalOptionOrder.forEach((option) => {
option.classList.remove("option-hidden");
fragment.appendChild(option);
});
optionsContainer.appendChild(fragment);
filterResults.style.display = "none";
return;
}
const searchTerms = searchTerm
.split(/\s+/)
.filter((term) => term.length > 0);
let visibleCount = 0;
const titleMatches = [];
const descMatches = [];
optionsData.forEach((data) => {
let isTitleMatch = false;
let isDescMatch = false;
if (searchTerms.length === 1) {
const term = searchTerms[0];
isTitleMatch = data.name.includes(term);
isDescMatch = !isTitleMatch && data.description.includes(term);
} else {
isTitleMatch = searchTerms.every((term) => data.name.includes(term));
isDescMatch = !isTitleMatch &&
searchTerms.every((term) => data.description.includes(term));
}
if (isTitleMatch) {
titleMatches.push(data);
} else if (isDescMatch) {
descMatches.push(data);
}
});
if (searchTerms.length === 1) {
const term = searchTerms[0];
titleMatches.sort(
(a, b) => a.name.indexOf(term) - b.name.indexOf(term),
);
descMatches.sort(
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
);
}
itemsToProcess = [];
titleMatches.forEach((data) => {
visibleCount++;
itemsToProcess.push({ element: data.element, visible: true });
});
descMatches.forEach((data) => {
visibleCount++;
itemsToProcess.push({ element: data.element, visible: true });
});
optionsData.forEach((data) => {
if (!itemsToProcess.some((item) => item.element === data.element)) {
itemsToProcess.push({ element: data.element, visible: false });
}
});
// Reorder DOM so all title matches, then desc matches, then hidden
const fragment = document.createDocumentFragment();
itemsToProcess.forEach((item) => {
fragment.appendChild(item.element);
});
optionsContainer.appendChild(fragment);
filterResults.visibleCount = visibleCount;
pendingRender = requestAnimationFrame(processNextChunk);
}
// Use different debounce times for desktop vs mobile
const debouncedFilter = debounce(filterOptions, isMobile ? 200 : 100);
// Set up event listeners
optionsFilter.addEventListener("input", debouncedFilter);
optionsFilter.addEventListener("change", filterOptions);
// Allow clearing with Escape key
optionsFilter.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
optionsFilter.value = "";
filterOptions();
}
});
// Handle visibility changes
document.addEventListener("visibilitychange", function () {
if (!document.hidden && optionsFilter.value) {
filterOptions();
}
});
// Initially trigger filter if there's a value
if (optionsFilter.value) {
filterOptions();
}
// Pre-calculate heights for smoother scrolling
if (isMobile && totalCount > 50) {
requestIdleCallback(() => {
const sampleOption = options[0];
if (sampleOption) {
const height = sampleOption.offsetHeight;
if (height > 0) {
options.forEach((opt) => {
opt.style.containIntrinsicSize = `0 ${height}px`;
});
}
}
});
}
}
});

File diff suppressed because one or more lines are too long

View file

@ -1,298 +0,0 @@
const isWordBoundary = (char) =>
/[A-Z]/.test(char) || /[-_\/.]/.test(char) || /\s/.test(char);
const isCaseTransition = (prev, curr) => {
const prevIsUpper = prev.toLowerCase() !== prev;
const currIsUpper = curr.toLowerCase() !== curr;
return (
prevIsUpper && currIsUpper && prev.toLowerCase() !== curr.toLowerCase()
);
};
const findBestSubsequenceMatch = (query, target) => {
const n = query.length;
const m = target.length;
if (n === 0 || m === 0) return null;
const positions = [];
const memo = new Map();
const key = (qIdx, tIdx, gap) => `${qIdx}:${tIdx}:${gap}`;
const findBest = (qIdx, tIdx, currentGap) => {
if (qIdx === n) {
return { done: true, positions: [...positions], gap: currentGap };
}
const memoKey = key(qIdx, tIdx, currentGap);
if (memo.has(memoKey)) {
return memo.get(memoKey);
}
let bestResult = null;
for (let i = tIdx; i < m; i++) {
if (target[i] === query[qIdx]) {
positions.push(i);
const gap = qIdx === 0 ? 0 : i - positions[positions.length - 2] - 1;
const newGap = currentGap + gap;
if (newGap > m) {
positions.pop();
continue;
}
const result = findBest(qIdx + 1, i + 1, newGap);
positions.pop();
if (result && (!bestResult || result.gap < bestResult.gap)) {
bestResult = result;
if (result.gap === 0) break;
}
}
}
memo.set(memoKey, bestResult);
return bestResult;
};
const result = findBest(0, 0, 0);
if (!result) return null;
const consecutive = (() => {
let c = 1;
for (let i = 1; i < result.positions.length; i++) {
if (result.positions[i] === result.positions[i - 1] + 1) {
c++;
}
}
return c;
})();
return {
positions: result.positions,
consecutive,
score: calculateMatchScore(query, target, result.positions, consecutive),
};
};
const calculateMatchScore = (query, target, positions, consecutive) => {
const n = positions.length;
const m = target.length;
if (n === 0) return 0;
let score = 1.0;
const startBonus = (m - positions[0]) / m;
score += startBonus * 0.5;
let gapPenalty = 0;
for (let i = 1; i < n; i++) {
const gap = positions[i] - positions[i - 1] - 1;
if (gap > 0) {
gapPenalty += Math.min(gap / m, 1.0) * 0.3;
}
}
score -= gapPenalty;
const consecutiveBonus = consecutive / n;
score += consecutiveBonus * 0.3;
let boundaryBonus = 0;
for (let i = 0; i < n; i++) {
const char = target[positions[i]];
if (i === 0 || isWordBoundary(char)) {
boundaryBonus += 0.05;
}
if (i > 0) {
const prevChar = target[positions[i - 1]];
if (isCaseTransition(prevChar, char)) {
boundaryBonus += 0.03;
}
}
}
score = Math.min(1.0, score + boundaryBonus);
const lengthPenalty = Math.abs(query.length - n) / Math.max(query.length, m);
score -= lengthPenalty * 0.2;
return Math.max(0, Math.min(1.0, score));
};
const fuzzyMatch = (query, target) => {
const lowerQuery = query.toLowerCase();
const lowerTarget = target.toLowerCase();
if (lowerQuery.length === 0) return null;
if (lowerTarget.length === 0) return null;
if (lowerTarget === lowerQuery) {
return 1.0;
}
if (lowerTarget.includes(lowerQuery)) {
const ratio = lowerQuery.length / lowerTarget.length;
return 0.8 + ratio * 0.2;
}
const match = findBestSubsequenceMatch(lowerQuery, lowerTarget);
if (!match) {
return null;
}
return Math.min(1.0, match.score);
};
self.onmessage = function (e) {
const { messageId, type, data } = e.data;
const respond = (type, data) => {
self.postMessage({ messageId, type, data });
};
const respondError = (error) => {
self.postMessage({
messageId,
type: "error",
error: error.message || String(error),
});
};
try {
if (type === "tokenize") {
const text = typeof data === "string" ? data : "";
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
const tokens = words.filter((word) => word.length > 2);
const uniqueTokens = Array.from(new Set(tokens));
respond("tokens", uniqueTokens);
} else if (type === "search") {
const { query, limit = 10 } = data;
if (!query || typeof query !== "string") {
respond("results", []);
return;
}
const rawQuery = query.toLowerCase();
const text = typeof query === "string" ? query : "";
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
const searchTerms = words.filter((word) => word.length > 2);
let documents = [];
if (typeof data.documents === "string") {
documents = JSON.parse(data.documents);
} else if (Array.isArray(data.documents)) {
documents = data.documents;
} else if (typeof data.transferables === "string") {
documents = JSON.parse(data.transferables);
}
if (!Array.isArray(documents) || documents.length === 0) {
respond("results", []);
return;
}
const useFuzzySearch = rawQuery.length >= 3;
if (searchTerms.length === 0 && rawQuery.length < 3) {
respond("results", []);
return;
}
const pageMatches = new Map();
// Pre-compute lower-case strings for each document
const processedDocs = documents.map((doc, docId) => {
const title = typeof doc.title === "string" ? doc.title : "";
const content = typeof doc.content === "string" ? doc.content : "";
return {
docId,
doc,
lowerTitle: title.toLowerCase(),
lowerContent: content.toLowerCase(),
};
});
// First pass: Score pages with fuzzy matching
processedDocs.forEach(({ docId, doc, lowerTitle, lowerContent }) => {
let match = pageMatches.get(docId);
if (!match) {
match = { doc, pageScore: 0, matchingAnchors: [] };
pageMatches.set(docId, match);
}
if (useFuzzySearch) {
const fuzzyTitleScore = fuzzyMatch(rawQuery, lowerTitle);
if (fuzzyTitleScore !== null) {
match.pageScore += fuzzyTitleScore * 100;
}
const fuzzyContentScore = fuzzyMatch(rawQuery, lowerContent);
if (fuzzyContentScore !== null) {
match.pageScore += fuzzyContentScore * 30;
}
}
// Token-based exact matching
searchTerms.forEach((term) => {
if (lowerTitle.includes(term)) {
match.pageScore += lowerTitle === term ? 20 : 10;
}
if (lowerContent.includes(term)) {
match.pageScore += 2;
}
});
});
// Second pass: Find matching anchors
pageMatches.forEach((match) => {
const doc = match.doc;
if (
!doc.anchors ||
!Array.isArray(doc.anchors) ||
doc.anchors.length === 0
) {
return;
}
doc.anchors.forEach((anchor) => {
if (!anchor || !anchor.text) return;
const anchorText = anchor.text.toLowerCase();
let anchorMatches = false;
if (useFuzzySearch) {
const fuzzyScore = fuzzyMatch(rawQuery, anchorText);
if (fuzzyScore !== null && fuzzyScore >= 0.4) {
anchorMatches = true;
}
}
if (!anchorMatches) {
searchTerms.forEach((term) => {
if (anchorText.includes(term)) {
anchorMatches = true;
}
});
}
if (anchorMatches) {
match.matchingAnchors.push(anchor);
}
});
});
const results = Array.from(pageMatches.values())
.filter((m) => m.pageScore > 5)
.sort((a, b) => b.pageScore - a.pageScore)
.slice(0, limit);
respond("results", results);
}
} catch (error) {
respondError(error);
}
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,153 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Known Issues and Quirks</title>
<script>
// Apply sidebar state immediately to prevent flash
(function () {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
})();
</script>
<link rel="stylesheet" href="assets/style.css" />
<script defer src="assets/main.js"></script>
<script>
window.searchNamespace = window.searchNamespace || {};
window.searchNamespace.rootPath = "";
</script>
<script defer src="assets/search.js"></script>
</head>
<body>
<div class="container">
<header>
<div class="header-left">
<h1 class="site-title">
<a href="index.html">NVF</a>
</h1>
<nav class="header-nav">
<ul>
<li >
<a href="options.html">Options</a>
</li>
<li><a href="search.html">Search</a></li>
</ul>
</nav>
</div>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search..." />
<div id="search-results" class="search-results"></div>
</div>
</header>
<div class="layout">
<div class="sidebar-toggle" aria-label="Toggle sidebar">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
</svg>
</div>
<nav class="sidebar">
<details class="sidebar-section" data-section="docs" open>
<summary>Documents</summary>
<div class="sidebar-section-content">
<ul>
<li><a href="index.html">Introduction</a></li>
<li><a href="configuring.html">Configuring nvf</a></li>
<li><a href="hacking.html">Hacking nvf</a></li>
<li><a href="tips.html">Helpful Tips</a></li>
<li><a href="quirks.html">Known Issues and Quirks</a></li>
<li><a href="release-notes.html">Release Notes</a></li>
<li><a href="search.html">Search</a></li>
</ul>
</div>
</details>
<details class="sidebar-section" data-section="toc" open>
<summary>Contents</summary>
<div class="sidebar-section-content">
<ul class="toc-list">
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
</ul><li><a href="#ch-bugs-suggestions">Bugs &amp; Suggestions</a>
</li></ul></li>
</ul>
</div>
</details>
</nav>
<main class="content"><html><head></head><body><h1 id="ch-known-issues-quirks">Known Issues and Quirks</h1>
<p>At times, certain plugins and modules may refuse to play nicely with your setup,
be it a result of generating Lua from Nix, or the state of packaging. This page,
in turn, will list any known modules or plugins that are known to misbehave, and
possible workarounds that you may apply.</p>
<h2 id="ch-quirks-nodejs">NodeJS</h2>
<h3 id="sec-eslint-plugin-prettier">eslint-plugin-prettier</h3>
<p>When working with NodeJS, which is <em>obviously</em> known for its meticulous
standards, most things are bound to work as expected but some projects, tools
and settings may fool the default configurations of tools provided by <strong>nvf</strong>.</p>
<p>If</p>
<p>If <a href="https://github.com/prettier/eslint-plugin-prettier">eslint-plugin-prettier</a> or similar is included, you might get a situation
where your Eslint configuration diagnoses your formatting according to its own
config (usually <code>.eslintrc.js</code>). The issue there is your formatting is made via
prettierd.</p>
<p>This results in auto-formatting relying on your prettier configuration, while
your Eslint configuration diagnoses formatting "issues" while it's
<a href="https://prettier.io/docs/en/comparison.html">not supposed to</a>. In the end, you get discrepancies between what your editor
does and what it wants.</p>
<p>Solutions are:</p>
<ol>
<li>Don't add a formatting config to Eslint, instead separate Prettier and
Eslint.</li>
<li>PR the repo in question to add an ESLint formatter, and configure <strong>nvf</strong> to
use it.</li>
</ol>
<h2 id="ch-bugs-suggestions">Bugs &amp; Suggestions</h2>
<p>Some quirks are not exactly quirks, but bugs in the module system. If you notice
any issues with <strong>nvf</strong>, or this documentation, then please consider reporting
them over at the <a href="https://github.com/notashelf/nvf/issues">issue tracker</a>. Issues tab, in addition to the
<a href="https://github.com/notashelf/nvf/discussions">discussions tab</a> is a good place as any to request new features.</p>
<p>You may also consider submitting bug fixes, feature additions and upstreamed
changes that you think are critical over at the <a href="https://github.com/notashelf/nvf/pulls">pull requests tab</a>.</p>
</body></html></main>
</div>
<aside class="page-toc">
<nav class="page-toc-nav">
<h3>On this page</h3>
<ul class="page-toc-list">
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
</ul><li><a href="#ch-bugs-suggestions">Bugs &amp; Suggestions</a>
</li></ul></li>
</ul>
</nav>
</aside>
<footer>
<p>Generated with ndg</p>
</footer>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,110 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NVF - Search</title>
<script>
// Apply sidebar state immediately to prevent flash
(function () {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
})();
</script>
<link rel="stylesheet" href="assets/style.css" />
<script defer src="assets/main.js"></script>
<script>
window.searchNamespace = window.searchNamespace || {};
window.searchNamespace.rootPath = "";
</script>
<script defer src="assets/search.js"></script>
</head>
<body>
<div class="container">
<header>
<div class="header-left">
<h1 class="site-title">
<a href="index.html">NVF</a>
</h1>
</div>
<nav class="header-nav">
<ul>
<li >
<a href="options.html">Options</a>
</li>
<li><a href="search.html">Search</a></li>
</ul>
</nav>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search..." />
<div id="search-results" class="search-results"></div>
</div>
</header>
<div class="layout">
<div class="sidebar-toggle" aria-label="Toggle sidebar">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
</svg>
</div>
<nav id="sidebar" class="sidebar">
<div class="docs-nav">
<h2>Documents</h2>
<ul>
<li><a href="index.html">Introduction</a></li>
<li><a href="configuring.html">Configuring nvf</a></li>
<li><a href="hacking.html">Hacking nvf</a></li>
<li><a href="tips.html">Helpful Tips</a></li>
<li><a href="quirks.html">Known Issues and Quirks</a></li>
<li><a href="release-notes.html">Release Notes</a></li>
<li><a href="search.html">Search</a></li>
</ul>
</div>
<div class="toc">
<h2>Contents</h2>
<ul class="toc-list">
</ul>
</div>
</nav>
<main class="content">
<h1>Search</h1>
<div class="search-page">
<div class="search-form">
<input
type="text"
id="search-page-input"
placeholder="Search..."
autofocus
/>
</div>
<div id="search-page-results" class="search-page-results"></div>
</div>
<div class="footnotes-container">
<!-- Footnotes will be appended here -->
</div>
</main>
</div>
<footer>
<p>Generated with ndg</p>
</footer>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,740 +0,0 @@
// Polyfill for requestIdleCallback for Safari and unsupported browsers
if (typeof window.requestIdleCallback === "undefined") {
window.requestIdleCallback = function (cb) {
const start = Date.now();
const idlePeriod = 50;
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, idlePeriod - (Date.now() - start));
},
});
}, 1);
};
window.cancelIdleCallback = function (id) {
clearTimeout(id);
};
}
// Create mobile elements if they don't exist
function createMobileElements() {
// Create mobile sidebar FAB
const mobileFab = document.createElement("button");
mobileFab.className = "mobile-sidebar-fab";
mobileFab.setAttribute("aria-label", "Toggle sidebar menu");
mobileFab.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
`;
// Only show FAB on mobile (max-width: 800px)
function updateFabVisibility() {
if (window.innerWidth > 800) {
if (mobileFab.parentNode) mobileFab.parentNode.removeChild(mobileFab);
} else {
if (!document.body.contains(mobileFab)) {
document.body.appendChild(mobileFab);
}
mobileFab.style.display = "flex";
}
}
updateFabVisibility();
window.addEventListener("resize", updateFabVisibility);
// Create mobile sidebar container
const mobileContainer = document.createElement("div");
mobileContainer.className = "mobile-sidebar-container";
mobileContainer.innerHTML = `
<div class="mobile-sidebar-handle">
<div class="mobile-sidebar-dragger"></div>
</div>
<div class="mobile-sidebar-content">
<!-- Sidebar content will be cloned here -->
</div>
`;
// Create mobile search popup
const mobileSearchPopup = document.createElement("div");
mobileSearchPopup.id = "mobile-search-popup";
mobileSearchPopup.className = "mobile-search-popup";
mobileSearchPopup.innerHTML = `
<div class="mobile-search-container">
<div class="mobile-search-header">
<input type="text" id="mobile-search-input" placeholder="Search..." />
<button id="close-mobile-search" class="close-mobile-search" aria-label="Close search">&times;</button>
</div>
<div id="mobile-search-results" class="mobile-search-results"></div>
</div>
`;
// Insert at end of body so it is not affected by .container flex or stacking context
document.body.appendChild(mobileContainer);
document.body.appendChild(mobileSearchPopup);
// Immediately populate mobile sidebar content if desktop sidebar exists
const desktopSidebar = document.querySelector(".sidebar");
const mobileSidebarContent = mobileContainer.querySelector(
".mobile-sidebar-content",
);
if (desktopSidebar && mobileSidebarContent) {
mobileSidebarContent.innerHTML = desktopSidebar.innerHTML;
}
}
// Initialize collapsible sidebar sections with state persistence
function initCollapsibleSections() {
// Target sections in both desktop and mobile sidebars
const sections = document.querySelectorAll(
".sidebar .sidebar-section, .mobile-sidebar-content .sidebar-section",
);
sections.forEach((section) => {
const sectionId = section.dataset.section;
if (!sectionId) return;
const storageKey = `sidebar-section-${sectionId}`;
const savedState = localStorage.getItem(storageKey);
// Restore saved state (default is open)
if (savedState === "closed") {
section.removeAttribute("open");
}
// Save state on toggle and sync between desktop/mobile
section.addEventListener("toggle", () => {
localStorage.setItem(storageKey, section.open ? "open" : "closed");
// Sync state between desktop and mobile versions
const allWithSameSection = document.querySelectorAll(
`.sidebar-section[data-section="${sectionId}"]`,
);
allWithSameSection.forEach((el) => {
if (el !== section) {
el.open = section.open;
}
});
});
});
}
// Initialize scroll spy
function initScrollSpy() {
const pageToc = document.querySelector(".page-toc");
if (!pageToc) return;
const tocLinks = pageToc.querySelectorAll(".page-toc-list a");
const content = document.querySelector(".content");
if (!tocLinks.length || !content) return;
const headings = Array.from(
content.querySelectorAll("h1[id], h2[id], h3[id]"),
);
if (!headings.length) return;
// Build a map of heading IDs to TOC links for quick lookup
const linkMap = new Map();
tocLinks.forEach((link) => {
const href = link.getAttribute("href");
if (href && href.startsWith("#")) {
linkMap.set(href.slice(1), link);
}
});
let activeLink = null;
// Update active link based on scroll position
function updateActiveLink() {
const threshold = 120; // threshold from the top of the viewport
let currentHeading = null;
// Find the last heading that is at or above the threshold
for (const heading of headings) {
const rect = heading.getBoundingClientRect();
if (rect.top <= threshold) {
currentHeading = heading;
}
}
// If no heading is above threshold, use first heading if it's in view
if (!currentHeading && headings.length > 0) {
const firstRect = headings[0].getBoundingClientRect();
if (firstRect.top < window.innerHeight) {
currentHeading = headings[0];
}
}
const newLink = currentHeading ? linkMap.get(currentHeading.id) : null;
if (newLink !== activeLink) {
if (activeLink) {
activeLink.classList.remove("active");
}
if (newLink) {
newLink.classList.add("active");
}
activeLink = newLink;
}
}
// Scroll event handler
let ticking = false;
function onScroll() {
if (!ticking) {
requestAnimationFrame(() => {
updateActiveLink();
ticking = false;
});
ticking = true;
}
}
window.addEventListener("scroll", onScroll, { passive: true });
// Also update on hash change (direct link navigation)
window.addEventListener("hashchange", () => {
requestAnimationFrame(updateActiveLink);
});
// Set initial active state after a small delay to ensure
// browser has completed any hash-based scrolling
setTimeout(updateActiveLink, 100);
}
document.addEventListener("DOMContentLoaded", function () {
// Apply sidebar state immediately before DOM rendering
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
document.body.classList.add("sidebar-collapsed");
}
if (!document.querySelector(".mobile-sidebar-fab")) {
createMobileElements();
}
// Initialize collapsible sidebar sections
// after mobile elements are created
initCollapsibleSections();
// Initialize scroll spy for page TOC
initScrollSpy();
// Desktop Sidebar Toggle
const sidebarToggle = document.querySelector(".sidebar-toggle");
// On page load, sync the state from `documentElement` to `body`
if (document.documentElement.classList.contains("sidebar-collapsed")) {
document.body.classList.add("sidebar-collapsed");
}
if (sidebarToggle) {
sidebarToggle.addEventListener("click", function () {
// Toggle on both elements for consistency
document.documentElement.classList.toggle("sidebar-collapsed");
document.body.classList.toggle("sidebar-collapsed");
// Use documentElement to check state and save to localStorage
const isCollapsed = document.documentElement.classList.contains(
"sidebar-collapsed",
);
localStorage.setItem("sidebar-collapsed", isCollapsed);
});
}
// Make headings clickable for anchor links
const content = document.querySelector(".content");
if (content) {
const headings = content.querySelectorAll("h1, h2, h3, h4, h5, h6");
headings.forEach(function (heading) {
// Generate a valid, unique ID for each heading
if (!heading.id) {
let baseId = heading.textContent
.toLowerCase()
.replace(/[^a-z0-9\s-_]/g, "") // remove invalid chars
.replace(/^[^a-z]+/, "") // remove leading non-letters
.replace(/[\s-_]+/g, "-")
.replace(/^-+|-+$/g, "") // trim leading/trailing dashes
.trim();
if (!baseId) {
baseId = "section";
}
let id = baseId;
let counter = 1;
while (document.getElementById(id)) {
id = `${baseId}-${counter++}`;
}
heading.id = id;
}
// Make the entire heading clickable
heading.addEventListener("click", function () {
const id = this.id;
history.pushState(null, null, "#" + id);
// Scroll with offset
const offset = this.getBoundingClientRect().top + window.scrollY - 80;
window.scrollTo({
top: offset,
behavior: "smooth",
});
});
});
}
// Process footnotes
if (content) {
const footnoteContainer = document.querySelector(".footnotes-container");
// Find all footnote references and create a footnotes section
const footnoteRefs = content.querySelectorAll('a[href^="#fn"]');
if (footnoteRefs.length > 0) {
const footnotesDiv = document.createElement("div");
footnotesDiv.className = "footnotes";
const footnotesHeading = document.createElement("h2");
footnotesHeading.textContent = "Footnotes";
footnotesDiv.appendChild(footnotesHeading);
const footnotesList = document.createElement("ol");
footnoteContainer.appendChild(footnotesDiv);
footnotesDiv.appendChild(footnotesList);
// Add footnotes
document.querySelectorAll(".footnote").forEach((footnote) => {
const id = footnote.id;
const content = footnote.innerHTML;
const li = document.createElement("li");
li.id = id;
li.innerHTML = content;
// Add backlink
const backlink = document.createElement("a");
backlink.href = "#fnref:" + id.replace("fn:", "");
backlink.className = "footnote-backlink";
backlink.textContent = "↩";
li.appendChild(backlink);
footnotesList.appendChild(li);
});
}
}
// Copy link functionality
document.querySelectorAll(".copy-link").forEach(function (copyLink) {
copyLink.addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
// Get option ID from parent element
const option = copyLink.closest(".option");
const optionId = option.id;
// Create URL with hash
const url = new URL(window.location.href);
url.hash = optionId;
// Copy to clipboard
navigator.clipboard
.writeText(url.toString())
.then(function () {
// Show feedback
const feedback = copyLink.nextElementSibling;
feedback.style.display = "inline";
// Hide after 2 seconds
setTimeout(function () {
feedback.style.display = "none";
}, 2000);
})
.catch(function (err) {
console.error("Could not copy link: ", err);
});
});
});
// Handle initial hash navigation
function scrollToElement(element) {
if (element) {
const offset = element.getBoundingClientRect().top + window.scrollY - 80;
window.scrollTo({
top: offset,
behavior: "smooth",
});
}
}
if (window.location.hash) {
const targetElement = document.querySelector(window.location.hash);
if (targetElement) {
setTimeout(() => scrollToElement(targetElement), 0);
// Add highlight class for options page
if (targetElement.classList.contains("option")) {
targetElement.classList.add("highlight");
}
}
}
// Mobile Sidebar Functionality
const mobileSidebarContainer = document.querySelector(
".mobile-sidebar-container",
);
const mobileSidebarFab = document.querySelector(".mobile-sidebar-fab");
const mobileSidebarHandle = document.querySelector(".mobile-sidebar-handle");
// Always set up FAB if it exists
if (mobileSidebarFab && mobileSidebarContainer) {
const openMobileSidebar = () => {
mobileSidebarContainer.classList.add("active");
mobileSidebarFab.setAttribute("aria-expanded", "true");
mobileSidebarContainer.setAttribute("aria-hidden", "false");
mobileSidebarFab.classList.add("fab-hidden"); // hide FAB when drawer is open
};
const closeMobileSidebar = () => {
mobileSidebarContainer.classList.remove("active");
mobileSidebarFab.setAttribute("aria-expanded", "false");
mobileSidebarContainer.setAttribute("aria-hidden", "true");
mobileSidebarFab.classList.remove("fab-hidden"); // Show FAB when drawer is closed
};
mobileSidebarFab.addEventListener("click", (e) => {
e.stopPropagation();
if (mobileSidebarContainer.classList.contains("active")) {
closeMobileSidebar();
} else {
openMobileSidebar();
}
});
// Only set up drag functionality if handle exists
if (mobileSidebarHandle) {
// Drag functionality
let isDragging = false;
let startY = 0;
let startHeight = 0;
// Cleanup function for drag interruption
function cleanupDrag() {
if (isDragging) {
isDragging = false;
mobileSidebarHandle.style.cursor = "grab";
document.body.style.userSelect = "";
}
}
mobileSidebarHandle.addEventListener("mousedown", (e) => {
isDragging = true;
startY = e.pageY;
startHeight = mobileSidebarContainer.offsetHeight;
mobileSidebarHandle.style.cursor = "grabbing";
document.body.style.userSelect = "none"; // prevent text selection
});
mobileSidebarHandle.addEventListener("touchstart", (e) => {
isDragging = true;
startY = e.touches[0].pageY;
startHeight = mobileSidebarContainer.offsetHeight;
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const deltaY = startY - e.pageY;
const newHeight = startHeight + deltaY;
const vh = window.innerHeight;
const minHeight = vh * 0.15;
const maxHeight = vh * 0.9;
if (newHeight >= minHeight && newHeight <= maxHeight) {
mobileSidebarContainer.style.height = `${newHeight}px`;
}
});
document.addEventListener("touchmove", (e) => {
if (!isDragging) return;
const deltaY = startY - e.touches[0].pageY;
const newHeight = startHeight + deltaY;
const vh = window.innerHeight;
const minHeight = vh * 0.15;
const maxHeight = vh * 0.9;
if (newHeight >= minHeight && newHeight <= maxHeight) {
mobileSidebarContainer.style.height = `${newHeight}px`;
}
});
document.addEventListener("mouseup", cleanupDrag);
document.addEventListener("touchend", cleanupDrag);
window.addEventListener("blur", cleanupDrag);
document.addEventListener("visibilitychange", function () {
if (document.hidden) cleanupDrag();
});
}
// Close on outside click
document.addEventListener("click", (event) => {
if (
mobileSidebarContainer.classList.contains("active") &&
!mobileSidebarContainer.contains(event.target) &&
!mobileSidebarFab.contains(event.target)
) {
closeMobileSidebar();
}
});
// Close on escape key
document.addEventListener("keydown", (event) => {
if (
event.key === "Escape" &&
mobileSidebarContainer.classList.contains("active")
) {
closeMobileSidebar();
}
});
}
// Options filter functionality
const optionsFilter = document.getElementById("options-filter");
if (optionsFilter) {
const optionsContainer = document.querySelector(".options-container");
if (!optionsContainer) return;
// Only inject the style if it doesn't already exist
if (!document.head.querySelector("style[data-options-hidden]")) {
const styleEl = document.createElement("style");
styleEl.setAttribute("data-options-hidden", "");
styleEl.textContent = ".option-hidden{display:none!important}";
document.head.appendChild(styleEl);
}
// Create filter results counter
const filterResults = document.createElement("div");
filterResults.className = "filter-results";
optionsFilter.parentNode.insertBefore(
filterResults,
optionsFilter.nextSibling,
);
// Detect if we're on a mobile device
const isMobile = window.innerWidth < 768 ||
/Mobi|Android/i.test(navigator.userAgent);
// Cache all option elements and their searchable content
const options = Array.from(document.querySelectorAll(".option"));
const totalCount = options.length;
// Store the original order of option elements
const originalOptionOrder = options.slice();
// Pre-process and optimize searchable content
const optionsData = options.map((option) => {
const nameElem = option.querySelector(".option-name");
const descriptionElem = option.querySelector(".option-description");
const id = option.id ? option.id.toLowerCase() : "";
const name = nameElem ? nameElem.textContent.toLowerCase() : "";
const description = descriptionElem
? descriptionElem.textContent.toLowerCase()
: "";
// Extract keywords for faster searching
const keywords = (id + " " + name + " " + description)
.toLowerCase()
.split(/\s+/)
.filter((word) => word.length > 1);
return {
element: option,
id,
name,
description,
keywords,
searchText: (id + " " + name + " " + description).toLowerCase(),
};
});
// Chunk size and rendering variables
const CHUNK_SIZE = isMobile ? 15 : 40;
let pendingRender = null;
let currentChunk = 0;
let itemsToProcess = [];
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// Process options in chunks to prevent UI freezing
function processNextChunk() {
const startIdx = currentChunk * CHUNK_SIZE;
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
if (startIdx < itemsToProcess.length) {
// Process current chunk
for (let i = startIdx; i < endIdx; i++) {
const item = itemsToProcess[i];
if (item.visible) {
item.element.classList.remove("option-hidden");
} else {
item.element.classList.add("option-hidden");
}
}
currentChunk++;
pendingRender = requestAnimationFrame(processNextChunk);
} else {
// Finished processing all chunks
pendingRender = null;
currentChunk = 0;
itemsToProcess = [];
// Update counter at the very end for best performance
if (filterResults.visibleCount !== undefined) {
if (filterResults.visibleCount < totalCount) {
filterResults.textContent =
`Showing ${filterResults.visibleCount} of ${totalCount} options`;
filterResults.style.display = "block";
} else {
filterResults.style.display = "none";
}
}
}
}
function filterOptions() {
const searchTerm = optionsFilter.value.toLowerCase().trim();
if (pendingRender) {
cancelAnimationFrame(pendingRender);
pendingRender = null;
}
currentChunk = 0;
itemsToProcess = [];
if (searchTerm === "") {
// Restore original DOM order when filter is cleared
const fragment = document.createDocumentFragment();
originalOptionOrder.forEach((option) => {
option.classList.remove("option-hidden");
fragment.appendChild(option);
});
optionsContainer.appendChild(fragment);
filterResults.style.display = "none";
return;
}
const searchTerms = searchTerm
.split(/\s+/)
.filter((term) => term.length > 0);
let visibleCount = 0;
const titleMatches = [];
const descMatches = [];
optionsData.forEach((data) => {
let isTitleMatch = false;
let isDescMatch = false;
if (searchTerms.length === 1) {
const term = searchTerms[0];
isTitleMatch = data.name.includes(term);
isDescMatch = !isTitleMatch && data.description.includes(term);
} else {
isTitleMatch = searchTerms.every((term) => data.name.includes(term));
isDescMatch = !isTitleMatch &&
searchTerms.every((term) => data.description.includes(term));
}
if (isTitleMatch) {
titleMatches.push(data);
} else if (isDescMatch) {
descMatches.push(data);
}
});
if (searchTerms.length === 1) {
const term = searchTerms[0];
titleMatches.sort(
(a, b) => a.name.indexOf(term) - b.name.indexOf(term),
);
descMatches.sort(
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
);
}
itemsToProcess = [];
titleMatches.forEach((data) => {
visibleCount++;
itemsToProcess.push({ element: data.element, visible: true });
});
descMatches.forEach((data) => {
visibleCount++;
itemsToProcess.push({ element: data.element, visible: true });
});
optionsData.forEach((data) => {
if (!itemsToProcess.some((item) => item.element === data.element)) {
itemsToProcess.push({ element: data.element, visible: false });
}
});
// Reorder DOM so all title matches, then desc matches, then hidden
const fragment = document.createDocumentFragment();
itemsToProcess.forEach((item) => {
fragment.appendChild(item.element);
});
optionsContainer.appendChild(fragment);
filterResults.visibleCount = visibleCount;
pendingRender = requestAnimationFrame(processNextChunk);
}
// Use different debounce times for desktop vs mobile
const debouncedFilter = debounce(filterOptions, isMobile ? 200 : 100);
// Set up event listeners
optionsFilter.addEventListener("input", debouncedFilter);
optionsFilter.addEventListener("change", filterOptions);
// Allow clearing with Escape key
optionsFilter.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
optionsFilter.value = "";
filterOptions();
}
});
// Handle visibility changes
document.addEventListener("visibilitychange", function () {
if (!document.hidden && optionsFilter.value) {
filterOptions();
}
});
// Initially trigger filter if there's a value
if (optionsFilter.value) {
filterOptions();
}
// Pre-calculate heights for smoother scrolling
if (isMobile && totalCount > 50) {
requestIdleCallback(() => {
const sampleOption = options[0];
if (sampleOption) {
const height = sampleOption.offsetHeight;
if (height > 0) {
options.forEach((opt) => {
opt.style.containIntrinsicSize = `0 ${height}px`;
});
}
}
});
}
}
});

File diff suppressed because one or more lines are too long

View file

@ -1,298 +0,0 @@
const isWordBoundary = (char) =>
/[A-Z]/.test(char) || /[-_\/.]/.test(char) || /\s/.test(char);
const isCaseTransition = (prev, curr) => {
const prevIsUpper = prev.toLowerCase() !== prev;
const currIsUpper = curr.toLowerCase() !== curr;
return (
prevIsUpper && currIsUpper && prev.toLowerCase() !== curr.toLowerCase()
);
};
const findBestSubsequenceMatch = (query, target) => {
const n = query.length;
const m = target.length;
if (n === 0 || m === 0) return null;
const positions = [];
const memo = new Map();
const key = (qIdx, tIdx, gap) => `${qIdx}:${tIdx}:${gap}`;
const findBest = (qIdx, tIdx, currentGap) => {
if (qIdx === n) {
return { done: true, positions: [...positions], gap: currentGap };
}
const memoKey = key(qIdx, tIdx, currentGap);
if (memo.has(memoKey)) {
return memo.get(memoKey);
}
let bestResult = null;
for (let i = tIdx; i < m; i++) {
if (target[i] === query[qIdx]) {
positions.push(i);
const gap = qIdx === 0 ? 0 : i - positions[positions.length - 2] - 1;
const newGap = currentGap + gap;
if (newGap > m) {
positions.pop();
continue;
}
const result = findBest(qIdx + 1, i + 1, newGap);
positions.pop();
if (result && (!bestResult || result.gap < bestResult.gap)) {
bestResult = result;
if (result.gap === 0) break;
}
}
}
memo.set(memoKey, bestResult);
return bestResult;
};
const result = findBest(0, 0, 0);
if (!result) return null;
const consecutive = (() => {
let c = 1;
for (let i = 1; i < result.positions.length; i++) {
if (result.positions[i] === result.positions[i - 1] + 1) {
c++;
}
}
return c;
})();
return {
positions: result.positions,
consecutive,
score: calculateMatchScore(query, target, result.positions, consecutive),
};
};
const calculateMatchScore = (query, target, positions, consecutive) => {
const n = positions.length;
const m = target.length;
if (n === 0) return 0;
let score = 1.0;
const startBonus = (m - positions[0]) / m;
score += startBonus * 0.5;
let gapPenalty = 0;
for (let i = 1; i < n; i++) {
const gap = positions[i] - positions[i - 1] - 1;
if (gap > 0) {
gapPenalty += Math.min(gap / m, 1.0) * 0.3;
}
}
score -= gapPenalty;
const consecutiveBonus = consecutive / n;
score += consecutiveBonus * 0.3;
let boundaryBonus = 0;
for (let i = 0; i < n; i++) {
const char = target[positions[i]];
if (i === 0 || isWordBoundary(char)) {
boundaryBonus += 0.05;
}
if (i > 0) {
const prevChar = target[positions[i - 1]];
if (isCaseTransition(prevChar, char)) {
boundaryBonus += 0.03;
}
}
}
score = Math.min(1.0, score + boundaryBonus);
const lengthPenalty = Math.abs(query.length - n) / Math.max(query.length, m);
score -= lengthPenalty * 0.2;
return Math.max(0, Math.min(1.0, score));
};
const fuzzyMatch = (query, target) => {
const lowerQuery = query.toLowerCase();
const lowerTarget = target.toLowerCase();
if (lowerQuery.length === 0) return null;
if (lowerTarget.length === 0) return null;
if (lowerTarget === lowerQuery) {
return 1.0;
}
if (lowerTarget.includes(lowerQuery)) {
const ratio = lowerQuery.length / lowerTarget.length;
return 0.8 + ratio * 0.2;
}
const match = findBestSubsequenceMatch(lowerQuery, lowerTarget);
if (!match) {
return null;
}
return Math.min(1.0, match.score);
};
self.onmessage = function (e) {
const { messageId, type, data } = e.data;
const respond = (type, data) => {
self.postMessage({ messageId, type, data });
};
const respondError = (error) => {
self.postMessage({
messageId,
type: "error",
error: error.message || String(error),
});
};
try {
if (type === "tokenize") {
const text = typeof data === "string" ? data : "";
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
const tokens = words.filter((word) => word.length > 2);
const uniqueTokens = Array.from(new Set(tokens));
respond("tokens", uniqueTokens);
} else if (type === "search") {
const { query, limit = 10 } = data;
if (!query || typeof query !== "string") {
respond("results", []);
return;
}
const rawQuery = query.toLowerCase();
const text = typeof query === "string" ? query : "";
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
const searchTerms = words.filter((word) => word.length > 2);
let documents = [];
if (typeof data.documents === "string") {
documents = JSON.parse(data.documents);
} else if (Array.isArray(data.documents)) {
documents = data.documents;
} else if (typeof data.transferables === "string") {
documents = JSON.parse(data.transferables);
}
if (!Array.isArray(documents) || documents.length === 0) {
respond("results", []);
return;
}
const useFuzzySearch = rawQuery.length >= 3;
if (searchTerms.length === 0 && rawQuery.length < 3) {
respond("results", []);
return;
}
const pageMatches = new Map();
// Pre-compute lower-case strings for each document
const processedDocs = documents.map((doc, docId) => {
const title = typeof doc.title === "string" ? doc.title : "";
const content = typeof doc.content === "string" ? doc.content : "";
return {
docId,
doc,
lowerTitle: title.toLowerCase(),
lowerContent: content.toLowerCase(),
};
});
// First pass: Score pages with fuzzy matching
processedDocs.forEach(({ docId, doc, lowerTitle, lowerContent }) => {
let match = pageMatches.get(docId);
if (!match) {
match = { doc, pageScore: 0, matchingAnchors: [] };
pageMatches.set(docId, match);
}
if (useFuzzySearch) {
const fuzzyTitleScore = fuzzyMatch(rawQuery, lowerTitle);
if (fuzzyTitleScore !== null) {
match.pageScore += fuzzyTitleScore * 100;
}
const fuzzyContentScore = fuzzyMatch(rawQuery, lowerContent);
if (fuzzyContentScore !== null) {
match.pageScore += fuzzyContentScore * 30;
}
}
// Token-based exact matching
searchTerms.forEach((term) => {
if (lowerTitle.includes(term)) {
match.pageScore += lowerTitle === term ? 20 : 10;
}
if (lowerContent.includes(term)) {
match.pageScore += 2;
}
});
});
// Second pass: Find matching anchors
pageMatches.forEach((match) => {
const doc = match.doc;
if (
!doc.anchors ||
!Array.isArray(doc.anchors) ||
doc.anchors.length === 0
) {
return;
}
doc.anchors.forEach((anchor) => {
if (!anchor || !anchor.text) return;
const anchorText = anchor.text.toLowerCase();
let anchorMatches = false;
if (useFuzzySearch) {
const fuzzyScore = fuzzyMatch(rawQuery, anchorText);
if (fuzzyScore !== null && fuzzyScore >= 0.4) {
anchorMatches = true;
}
}
if (!anchorMatches) {
searchTerms.forEach((term) => {
if (anchorText.includes(term)) {
anchorMatches = true;
}
});
}
if (anchorMatches) {
match.matchingAnchors.push(anchor);
}
});
});
const results = Array.from(pageMatches.values())
.filter((m) => m.pageScore > 5)
.sort((a, b) => b.pageScore - a.pageScore)
.slice(0, limit);
respond("results", results);
}
} catch (error) {
respondError(error);
}
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,153 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Known Issues and Quirks</title>
<script>
// Apply sidebar state immediately to prevent flash
(function () {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
})();
</script>
<link rel="stylesheet" href="assets/style.css" />
<script defer src="assets/main.js"></script>
<script>
window.searchNamespace = window.searchNamespace || {};
window.searchNamespace.rootPath = "";
</script>
<script defer src="assets/search.js"></script>
</head>
<body>
<div class="container">
<header>
<div class="header-left">
<h1 class="site-title">
<a href="index.html">NVF</a>
</h1>
<nav class="header-nav">
<ul>
<li >
<a href="options.html">Options</a>
</li>
<li><a href="search.html">Search</a></li>
</ul>
</nav>
</div>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search..." />
<div id="search-results" class="search-results"></div>
</div>
</header>
<div class="layout">
<div class="sidebar-toggle" aria-label="Toggle sidebar">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
</svg>
</div>
<nav class="sidebar">
<details class="sidebar-section" data-section="docs" open>
<summary>Documents</summary>
<div class="sidebar-section-content">
<ul>
<li><a href="index.html">Introduction</a></li>
<li><a href="configuring.html">Configuring nvf</a></li>
<li><a href="hacking.html">Hacking nvf</a></li>
<li><a href="tips.html">Helpful Tips</a></li>
<li><a href="quirks.html">Known Issues and Quirks</a></li>
<li><a href="release-notes.html">Release Notes</a></li>
<li><a href="search.html">Search</a></li>
</ul>
</div>
</details>
<details class="sidebar-section" data-section="toc" open>
<summary>Contents</summary>
<div class="sidebar-section-content">
<ul class="toc-list">
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
</ul><li><a href="#ch-bugs-suggestions">Bugs &amp; Suggestions</a>
</li></ul></li>
</ul>
</div>
</details>
</nav>
<main class="content"><html><head></head><body><h1 id="ch-known-issues-quirks">Known Issues and Quirks</h1>
<p>At times, certain plugins and modules may refuse to play nicely with your setup,
be it a result of generating Lua from Nix, or the state of packaging. This page,
in turn, will list any known modules or plugins that are known to misbehave, and
possible workarounds that you may apply.</p>
<h2 id="ch-quirks-nodejs">NodeJS</h2>
<h3 id="sec-eslint-plugin-prettier">eslint-plugin-prettier</h3>
<p>When working with NodeJS, which is <em>obviously</em> known for its meticulous
standards, most things are bound to work as expected but some projects, tools
and settings may fool the default configurations of tools provided by <strong>nvf</strong>.</p>
<p>If</p>
<p>If <a href="https://github.com/prettier/eslint-plugin-prettier">eslint-plugin-prettier</a> or similar is included, you might get a situation
where your Eslint configuration diagnoses your formatting according to its own
config (usually <code>.eslintrc.js</code>). The issue there is your formatting is made via
prettierd.</p>
<p>This results in auto-formatting relying on your prettier configuration, while
your Eslint configuration diagnoses formatting "issues" while it's
<a href="https://prettier.io/docs/en/comparison.html">not supposed to</a>. In the end, you get discrepancies between what your editor
does and what it wants.</p>
<p>Solutions are:</p>
<ol>
<li>Don't add a formatting config to Eslint, instead separate Prettier and
Eslint.</li>
<li>PR the repo in question to add an ESLint formatter, and configure <strong>nvf</strong> to
use it.</li>
</ol>
<h2 id="ch-bugs-suggestions">Bugs &amp; Suggestions</h2>
<p>Some quirks are not exactly quirks, but bugs in the module system. If you notice
any issues with <strong>nvf</strong>, or this documentation, then please consider reporting
them over at the <a href="https://github.com/notashelf/nvf/issues">issue tracker</a>. Issues tab, in addition to the
<a href="https://github.com/notashelf/nvf/discussions">discussions tab</a> is a good place as any to request new features.</p>
<p>You may also consider submitting bug fixes, feature additions and upstreamed
changes that you think are critical over at the <a href="https://github.com/notashelf/nvf/pulls">pull requests tab</a>.</p>
</body></html></main>
</div>
<aside class="page-toc">
<nav class="page-toc-nav">
<h3>On this page</h3>
<ul class="page-toc-list">
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
</ul><li><a href="#ch-bugs-suggestions">Bugs &amp; Suggestions</a>
</li></ul></li>
</ul>
</nav>
</aside>
<footer>
<p>Generated with ndg</p>
</footer>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,110 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NVF - Search</title>
<script>
// Apply sidebar state immediately to prevent flash
(function () {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
})();
</script>
<link rel="stylesheet" href="assets/style.css" />
<script defer src="assets/main.js"></script>
<script>
window.searchNamespace = window.searchNamespace || {};
window.searchNamespace.rootPath = "";
</script>
<script defer src="assets/search.js"></script>
</head>
<body>
<div class="container">
<header>
<div class="header-left">
<h1 class="site-title">
<a href="index.html">NVF</a>
</h1>
</div>
<nav class="header-nav">
<ul>
<li >
<a href="options.html">Options</a>
</li>
<li><a href="search.html">Search</a></li>
</ul>
</nav>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search..." />
<div id="search-results" class="search-results"></div>
</div>
</header>
<div class="layout">
<div class="sidebar-toggle" aria-label="Toggle sidebar">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
</svg>
</div>
<nav id="sidebar" class="sidebar">
<div class="docs-nav">
<h2>Documents</h2>
<ul>
<li><a href="index.html">Introduction</a></li>
<li><a href="configuring.html">Configuring nvf</a></li>
<li><a href="hacking.html">Hacking nvf</a></li>
<li><a href="tips.html">Helpful Tips</a></li>
<li><a href="quirks.html">Known Issues and Quirks</a></li>
<li><a href="release-notes.html">Release Notes</a></li>
<li><a href="search.html">Search</a></li>
</ul>
</div>
<div class="toc">
<h2>Contents</h2>
<ul class="toc-list">
</ul>
</div>
</nav>
<main class="content">
<h1>Search</h1>
<div class="search-page">
<div class="search-form">
<input
type="text"
id="search-page-input"
placeholder="Search..."
autofocus
/>
</div>
<div id="search-page-results" class="search-page-results"></div>
</div>
<div class="footnotes-container">
<!-- Footnotes will be appended here -->
</div>
</main>
</div>
<footer>
<p>Generated with ndg</p>
</footer>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,740 +0,0 @@
// Polyfill for requestIdleCallback for Safari and unsupported browsers
if (typeof window.requestIdleCallback === "undefined") {
window.requestIdleCallback = function (cb) {
const start = Date.now();
const idlePeriod = 50;
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, idlePeriod - (Date.now() - start));
},
});
}, 1);
};
window.cancelIdleCallback = function (id) {
clearTimeout(id);
};
}
// Create mobile elements if they don't exist
function createMobileElements() {
// Create mobile sidebar FAB
const mobileFab = document.createElement("button");
mobileFab.className = "mobile-sidebar-fab";
mobileFab.setAttribute("aria-label", "Toggle sidebar menu");
mobileFab.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
`;
// Only show FAB on mobile (max-width: 800px)
function updateFabVisibility() {
if (window.innerWidth > 800) {
if (mobileFab.parentNode) mobileFab.parentNode.removeChild(mobileFab);
} else {
if (!document.body.contains(mobileFab)) {
document.body.appendChild(mobileFab);
}
mobileFab.style.display = "flex";
}
}
updateFabVisibility();
window.addEventListener("resize", updateFabVisibility);
// Create mobile sidebar container
const mobileContainer = document.createElement("div");
mobileContainer.className = "mobile-sidebar-container";
mobileContainer.innerHTML = `
<div class="mobile-sidebar-handle">
<div class="mobile-sidebar-dragger"></div>
</div>
<div class="mobile-sidebar-content">
<!-- Sidebar content will be cloned here -->
</div>
`;
// Create mobile search popup
const mobileSearchPopup = document.createElement("div");
mobileSearchPopup.id = "mobile-search-popup";
mobileSearchPopup.className = "mobile-search-popup";
mobileSearchPopup.innerHTML = `
<div class="mobile-search-container">
<div class="mobile-search-header">
<input type="text" id="mobile-search-input" placeholder="Search..." />
<button id="close-mobile-search" class="close-mobile-search" aria-label="Close search">&times;</button>
</div>
<div id="mobile-search-results" class="mobile-search-results"></div>
</div>
`;
// Insert at end of body so it is not affected by .container flex or stacking context
document.body.appendChild(mobileContainer);
document.body.appendChild(mobileSearchPopup);
// Immediately populate mobile sidebar content if desktop sidebar exists
const desktopSidebar = document.querySelector(".sidebar");
const mobileSidebarContent = mobileContainer.querySelector(
".mobile-sidebar-content",
);
if (desktopSidebar && mobileSidebarContent) {
mobileSidebarContent.innerHTML = desktopSidebar.innerHTML;
}
}
// Initialize collapsible sidebar sections with state persistence
function initCollapsibleSections() {
// Target sections in both desktop and mobile sidebars
const sections = document.querySelectorAll(
".sidebar .sidebar-section, .mobile-sidebar-content .sidebar-section",
);
sections.forEach((section) => {
const sectionId = section.dataset.section;
if (!sectionId) return;
const storageKey = `sidebar-section-${sectionId}`;
const savedState = localStorage.getItem(storageKey);
// Restore saved state (default is open)
if (savedState === "closed") {
section.removeAttribute("open");
}
// Save state on toggle and sync between desktop/mobile
section.addEventListener("toggle", () => {
localStorage.setItem(storageKey, section.open ? "open" : "closed");
// Sync state between desktop and mobile versions
const allWithSameSection = document.querySelectorAll(
`.sidebar-section[data-section="${sectionId}"]`,
);
allWithSameSection.forEach((el) => {
if (el !== section) {
el.open = section.open;
}
});
});
});
}
// Initialize scroll spy
function initScrollSpy() {
const pageToc = document.querySelector(".page-toc");
if (!pageToc) return;
const tocLinks = pageToc.querySelectorAll(".page-toc-list a");
const content = document.querySelector(".content");
if (!tocLinks.length || !content) return;
const headings = Array.from(
content.querySelectorAll("h1[id], h2[id], h3[id]"),
);
if (!headings.length) return;
// Build a map of heading IDs to TOC links for quick lookup
const linkMap = new Map();
tocLinks.forEach((link) => {
const href = link.getAttribute("href");
if (href && href.startsWith("#")) {
linkMap.set(href.slice(1), link);
}
});
let activeLink = null;
// Update active link based on scroll position
function updateActiveLink() {
const threshold = 120; // threshold from the top of the viewport
let currentHeading = null;
// Find the last heading that is at or above the threshold
for (const heading of headings) {
const rect = heading.getBoundingClientRect();
if (rect.top <= threshold) {
currentHeading = heading;
}
}
// If no heading is above threshold, use first heading if it's in view
if (!currentHeading && headings.length > 0) {
const firstRect = headings[0].getBoundingClientRect();
if (firstRect.top < window.innerHeight) {
currentHeading = headings[0];
}
}
const newLink = currentHeading ? linkMap.get(currentHeading.id) : null;
if (newLink !== activeLink) {
if (activeLink) {
activeLink.classList.remove("active");
}
if (newLink) {
newLink.classList.add("active");
}
activeLink = newLink;
}
}
// Scroll event handler
let ticking = false;
function onScroll() {
if (!ticking) {
requestAnimationFrame(() => {
updateActiveLink();
ticking = false;
});
ticking = true;
}
}
window.addEventListener("scroll", onScroll, { passive: true });
// Also update on hash change (direct link navigation)
window.addEventListener("hashchange", () => {
requestAnimationFrame(updateActiveLink);
});
// Set initial active state after a small delay to ensure
// browser has completed any hash-based scrolling
setTimeout(updateActiveLink, 100);
}
document.addEventListener("DOMContentLoaded", function () {
// Apply sidebar state immediately before DOM rendering
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
document.body.classList.add("sidebar-collapsed");
}
if (!document.querySelector(".mobile-sidebar-fab")) {
createMobileElements();
}
// Initialize collapsible sidebar sections
// after mobile elements are created
initCollapsibleSections();
// Initialize scroll spy for page TOC
initScrollSpy();
// Desktop Sidebar Toggle
const sidebarToggle = document.querySelector(".sidebar-toggle");
// On page load, sync the state from `documentElement` to `body`
if (document.documentElement.classList.contains("sidebar-collapsed")) {
document.body.classList.add("sidebar-collapsed");
}
if (sidebarToggle) {
sidebarToggle.addEventListener("click", function () {
// Toggle on both elements for consistency
document.documentElement.classList.toggle("sidebar-collapsed");
document.body.classList.toggle("sidebar-collapsed");
// Use documentElement to check state and save to localStorage
const isCollapsed = document.documentElement.classList.contains(
"sidebar-collapsed",
);
localStorage.setItem("sidebar-collapsed", isCollapsed);
});
}
// Make headings clickable for anchor links
const content = document.querySelector(".content");
if (content) {
const headings = content.querySelectorAll("h1, h2, h3, h4, h5, h6");
headings.forEach(function (heading) {
// Generate a valid, unique ID for each heading
if (!heading.id) {
let baseId = heading.textContent
.toLowerCase()
.replace(/[^a-z0-9\s-_]/g, "") // remove invalid chars
.replace(/^[^a-z]+/, "") // remove leading non-letters
.replace(/[\s-_]+/g, "-")
.replace(/^-+|-+$/g, "") // trim leading/trailing dashes
.trim();
if (!baseId) {
baseId = "section";
}
let id = baseId;
let counter = 1;
while (document.getElementById(id)) {
id = `${baseId}-${counter++}`;
}
heading.id = id;
}
// Make the entire heading clickable
heading.addEventListener("click", function () {
const id = this.id;
history.pushState(null, null, "#" + id);
// Scroll with offset
const offset = this.getBoundingClientRect().top + window.scrollY - 80;
window.scrollTo({
top: offset,
behavior: "smooth",
});
});
});
}
// Process footnotes
if (content) {
const footnoteContainer = document.querySelector(".footnotes-container");
// Find all footnote references and create a footnotes section
const footnoteRefs = content.querySelectorAll('a[href^="#fn"]');
if (footnoteRefs.length > 0) {
const footnotesDiv = document.createElement("div");
footnotesDiv.className = "footnotes";
const footnotesHeading = document.createElement("h2");
footnotesHeading.textContent = "Footnotes";
footnotesDiv.appendChild(footnotesHeading);
const footnotesList = document.createElement("ol");
footnoteContainer.appendChild(footnotesDiv);
footnotesDiv.appendChild(footnotesList);
// Add footnotes
document.querySelectorAll(".footnote").forEach((footnote) => {
const id = footnote.id;
const content = footnote.innerHTML;
const li = document.createElement("li");
li.id = id;
li.innerHTML = content;
// Add backlink
const backlink = document.createElement("a");
backlink.href = "#fnref:" + id.replace("fn:", "");
backlink.className = "footnote-backlink";
backlink.textContent = "↩";
li.appendChild(backlink);
footnotesList.appendChild(li);
});
}
}
// Copy link functionality
document.querySelectorAll(".copy-link").forEach(function (copyLink) {
copyLink.addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
// Get option ID from parent element
const option = copyLink.closest(".option");
const optionId = option.id;
// Create URL with hash
const url = new URL(window.location.href);
url.hash = optionId;
// Copy to clipboard
navigator.clipboard
.writeText(url.toString())
.then(function () {
// Show feedback
const feedback = copyLink.nextElementSibling;
feedback.style.display = "inline";
// Hide after 2 seconds
setTimeout(function () {
feedback.style.display = "none";
}, 2000);
})
.catch(function (err) {
console.error("Could not copy link: ", err);
});
});
});
// Handle initial hash navigation
function scrollToElement(element) {
if (element) {
const offset = element.getBoundingClientRect().top + window.scrollY - 80;
window.scrollTo({
top: offset,
behavior: "smooth",
});
}
}
if (window.location.hash) {
const targetElement = document.querySelector(window.location.hash);
if (targetElement) {
setTimeout(() => scrollToElement(targetElement), 0);
// Add highlight class for options page
if (targetElement.classList.contains("option")) {
targetElement.classList.add("highlight");
}
}
}
// Mobile Sidebar Functionality
const mobileSidebarContainer = document.querySelector(
".mobile-sidebar-container",
);
const mobileSidebarFab = document.querySelector(".mobile-sidebar-fab");
const mobileSidebarHandle = document.querySelector(".mobile-sidebar-handle");
// Always set up FAB if it exists
if (mobileSidebarFab && mobileSidebarContainer) {
const openMobileSidebar = () => {
mobileSidebarContainer.classList.add("active");
mobileSidebarFab.setAttribute("aria-expanded", "true");
mobileSidebarContainer.setAttribute("aria-hidden", "false");
mobileSidebarFab.classList.add("fab-hidden"); // hide FAB when drawer is open
};
const closeMobileSidebar = () => {
mobileSidebarContainer.classList.remove("active");
mobileSidebarFab.setAttribute("aria-expanded", "false");
mobileSidebarContainer.setAttribute("aria-hidden", "true");
mobileSidebarFab.classList.remove("fab-hidden"); // Show FAB when drawer is closed
};
mobileSidebarFab.addEventListener("click", (e) => {
e.stopPropagation();
if (mobileSidebarContainer.classList.contains("active")) {
closeMobileSidebar();
} else {
openMobileSidebar();
}
});
// Only set up drag functionality if handle exists
if (mobileSidebarHandle) {
// Drag functionality
let isDragging = false;
let startY = 0;
let startHeight = 0;
// Cleanup function for drag interruption
function cleanupDrag() {
if (isDragging) {
isDragging = false;
mobileSidebarHandle.style.cursor = "grab";
document.body.style.userSelect = "";
}
}
mobileSidebarHandle.addEventListener("mousedown", (e) => {
isDragging = true;
startY = e.pageY;
startHeight = mobileSidebarContainer.offsetHeight;
mobileSidebarHandle.style.cursor = "grabbing";
document.body.style.userSelect = "none"; // prevent text selection
});
mobileSidebarHandle.addEventListener("touchstart", (e) => {
isDragging = true;
startY = e.touches[0].pageY;
startHeight = mobileSidebarContainer.offsetHeight;
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const deltaY = startY - e.pageY;
const newHeight = startHeight + deltaY;
const vh = window.innerHeight;
const minHeight = vh * 0.15;
const maxHeight = vh * 0.9;
if (newHeight >= minHeight && newHeight <= maxHeight) {
mobileSidebarContainer.style.height = `${newHeight}px`;
}
});
document.addEventListener("touchmove", (e) => {
if (!isDragging) return;
const deltaY = startY - e.touches[0].pageY;
const newHeight = startHeight + deltaY;
const vh = window.innerHeight;
const minHeight = vh * 0.15;
const maxHeight = vh * 0.9;
if (newHeight >= minHeight && newHeight <= maxHeight) {
mobileSidebarContainer.style.height = `${newHeight}px`;
}
});
document.addEventListener("mouseup", cleanupDrag);
document.addEventListener("touchend", cleanupDrag);
window.addEventListener("blur", cleanupDrag);
document.addEventListener("visibilitychange", function () {
if (document.hidden) cleanupDrag();
});
}
// Close on outside click
document.addEventListener("click", (event) => {
if (
mobileSidebarContainer.classList.contains("active") &&
!mobileSidebarContainer.contains(event.target) &&
!mobileSidebarFab.contains(event.target)
) {
closeMobileSidebar();
}
});
// Close on escape key
document.addEventListener("keydown", (event) => {
if (
event.key === "Escape" &&
mobileSidebarContainer.classList.contains("active")
) {
closeMobileSidebar();
}
});
}
// Options filter functionality
const optionsFilter = document.getElementById("options-filter");
if (optionsFilter) {
const optionsContainer = document.querySelector(".options-container");
if (!optionsContainer) return;
// Only inject the style if it doesn't already exist
if (!document.head.querySelector("style[data-options-hidden]")) {
const styleEl = document.createElement("style");
styleEl.setAttribute("data-options-hidden", "");
styleEl.textContent = ".option-hidden{display:none!important}";
document.head.appendChild(styleEl);
}
// Create filter results counter
const filterResults = document.createElement("div");
filterResults.className = "filter-results";
optionsFilter.parentNode.insertBefore(
filterResults,
optionsFilter.nextSibling,
);
// Detect if we're on a mobile device
const isMobile = window.innerWidth < 768 ||
/Mobi|Android/i.test(navigator.userAgent);
// Cache all option elements and their searchable content
const options = Array.from(document.querySelectorAll(".option"));
const totalCount = options.length;
// Store the original order of option elements
const originalOptionOrder = options.slice();
// Pre-process and optimize searchable content
const optionsData = options.map((option) => {
const nameElem = option.querySelector(".option-name");
const descriptionElem = option.querySelector(".option-description");
const id = option.id ? option.id.toLowerCase() : "";
const name = nameElem ? nameElem.textContent.toLowerCase() : "";
const description = descriptionElem
? descriptionElem.textContent.toLowerCase()
: "";
// Extract keywords for faster searching
const keywords = (id + " " + name + " " + description)
.toLowerCase()
.split(/\s+/)
.filter((word) => word.length > 1);
return {
element: option,
id,
name,
description,
keywords,
searchText: (id + " " + name + " " + description).toLowerCase(),
};
});
// Chunk size and rendering variables
const CHUNK_SIZE = isMobile ? 15 : 40;
let pendingRender = null;
let currentChunk = 0;
let itemsToProcess = [];
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// Process options in chunks to prevent UI freezing
function processNextChunk() {
const startIdx = currentChunk * CHUNK_SIZE;
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
if (startIdx < itemsToProcess.length) {
// Process current chunk
for (let i = startIdx; i < endIdx; i++) {
const item = itemsToProcess[i];
if (item.visible) {
item.element.classList.remove("option-hidden");
} else {
item.element.classList.add("option-hidden");
}
}
currentChunk++;
pendingRender = requestAnimationFrame(processNextChunk);
} else {
// Finished processing all chunks
pendingRender = null;
currentChunk = 0;
itemsToProcess = [];
// Update counter at the very end for best performance
if (filterResults.visibleCount !== undefined) {
if (filterResults.visibleCount < totalCount) {
filterResults.textContent =
`Showing ${filterResults.visibleCount} of ${totalCount} options`;
filterResults.style.display = "block";
} else {
filterResults.style.display = "none";
}
}
}
}
function filterOptions() {
const searchTerm = optionsFilter.value.toLowerCase().trim();
if (pendingRender) {
cancelAnimationFrame(pendingRender);
pendingRender = null;
}
currentChunk = 0;
itemsToProcess = [];
if (searchTerm === "") {
// Restore original DOM order when filter is cleared
const fragment = document.createDocumentFragment();
originalOptionOrder.forEach((option) => {
option.classList.remove("option-hidden");
fragment.appendChild(option);
});
optionsContainer.appendChild(fragment);
filterResults.style.display = "none";
return;
}
const searchTerms = searchTerm
.split(/\s+/)
.filter((term) => term.length > 0);
let visibleCount = 0;
const titleMatches = [];
const descMatches = [];
optionsData.forEach((data) => {
let isTitleMatch = false;
let isDescMatch = false;
if (searchTerms.length === 1) {
const term = searchTerms[0];
isTitleMatch = data.name.includes(term);
isDescMatch = !isTitleMatch && data.description.includes(term);
} else {
isTitleMatch = searchTerms.every((term) => data.name.includes(term));
isDescMatch = !isTitleMatch &&
searchTerms.every((term) => data.description.includes(term));
}
if (isTitleMatch) {
titleMatches.push(data);
} else if (isDescMatch) {
descMatches.push(data);
}
});
if (searchTerms.length === 1) {
const term = searchTerms[0];
titleMatches.sort(
(a, b) => a.name.indexOf(term) - b.name.indexOf(term),
);
descMatches.sort(
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
);
}
itemsToProcess = [];
titleMatches.forEach((data) => {
visibleCount++;
itemsToProcess.push({ element: data.element, visible: true });
});
descMatches.forEach((data) => {
visibleCount++;
itemsToProcess.push({ element: data.element, visible: true });
});
optionsData.forEach((data) => {
if (!itemsToProcess.some((item) => item.element === data.element)) {
itemsToProcess.push({ element: data.element, visible: false });
}
});
// Reorder DOM so all title matches, then desc matches, then hidden
const fragment = document.createDocumentFragment();
itemsToProcess.forEach((item) => {
fragment.appendChild(item.element);
});
optionsContainer.appendChild(fragment);
filterResults.visibleCount = visibleCount;
pendingRender = requestAnimationFrame(processNextChunk);
}
// Use different debounce times for desktop vs mobile
const debouncedFilter = debounce(filterOptions, isMobile ? 200 : 100);
// Set up event listeners
optionsFilter.addEventListener("input", debouncedFilter);
optionsFilter.addEventListener("change", filterOptions);
// Allow clearing with Escape key
optionsFilter.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
optionsFilter.value = "";
filterOptions();
}
});
// Handle visibility changes
document.addEventListener("visibilitychange", function () {
if (!document.hidden && optionsFilter.value) {
filterOptions();
}
});
// Initially trigger filter if there's a value
if (optionsFilter.value) {
filterOptions();
}
// Pre-calculate heights for smoother scrolling
if (isMobile && totalCount > 50) {
requestIdleCallback(() => {
const sampleOption = options[0];
if (sampleOption) {
const height = sampleOption.offsetHeight;
if (height > 0) {
options.forEach((opt) => {
opt.style.containIntrinsicSize = `0 ${height}px`;
});
}
}
});
}
}
});

File diff suppressed because one or more lines are too long

View file

@ -1,298 +0,0 @@
const isWordBoundary = (char) =>
/[A-Z]/.test(char) || /[-_\/.]/.test(char) || /\s/.test(char);
const isCaseTransition = (prev, curr) => {
const prevIsUpper = prev.toLowerCase() !== prev;
const currIsUpper = curr.toLowerCase() !== curr;
return (
prevIsUpper && currIsUpper && prev.toLowerCase() !== curr.toLowerCase()
);
};
const findBestSubsequenceMatch = (query, target) => {
const n = query.length;
const m = target.length;
if (n === 0 || m === 0) return null;
const positions = [];
const memo = new Map();
const key = (qIdx, tIdx, gap) => `${qIdx}:${tIdx}:${gap}`;
const findBest = (qIdx, tIdx, currentGap) => {
if (qIdx === n) {
return { done: true, positions: [...positions], gap: currentGap };
}
const memoKey = key(qIdx, tIdx, currentGap);
if (memo.has(memoKey)) {
return memo.get(memoKey);
}
let bestResult = null;
for (let i = tIdx; i < m; i++) {
if (target[i] === query[qIdx]) {
positions.push(i);
const gap = qIdx === 0 ? 0 : i - positions[positions.length - 2] - 1;
const newGap = currentGap + gap;
if (newGap > m) {
positions.pop();
continue;
}
const result = findBest(qIdx + 1, i + 1, newGap);
positions.pop();
if (result && (!bestResult || result.gap < bestResult.gap)) {
bestResult = result;
if (result.gap === 0) break;
}
}
}
memo.set(memoKey, bestResult);
return bestResult;
};
const result = findBest(0, 0, 0);
if (!result) return null;
const consecutive = (() => {
let c = 1;
for (let i = 1; i < result.positions.length; i++) {
if (result.positions[i] === result.positions[i - 1] + 1) {
c++;
}
}
return c;
})();
return {
positions: result.positions,
consecutive,
score: calculateMatchScore(query, target, result.positions, consecutive),
};
};
const calculateMatchScore = (query, target, positions, consecutive) => {
const n = positions.length;
const m = target.length;
if (n === 0) return 0;
let score = 1.0;
const startBonus = (m - positions[0]) / m;
score += startBonus * 0.5;
let gapPenalty = 0;
for (let i = 1; i < n; i++) {
const gap = positions[i] - positions[i - 1] - 1;
if (gap > 0) {
gapPenalty += Math.min(gap / m, 1.0) * 0.3;
}
}
score -= gapPenalty;
const consecutiveBonus = consecutive / n;
score += consecutiveBonus * 0.3;
let boundaryBonus = 0;
for (let i = 0; i < n; i++) {
const char = target[positions[i]];
if (i === 0 || isWordBoundary(char)) {
boundaryBonus += 0.05;
}
if (i > 0) {
const prevChar = target[positions[i - 1]];
if (isCaseTransition(prevChar, char)) {
boundaryBonus += 0.03;
}
}
}
score = Math.min(1.0, score + boundaryBonus);
const lengthPenalty = Math.abs(query.length - n) / Math.max(query.length, m);
score -= lengthPenalty * 0.2;
return Math.max(0, Math.min(1.0, score));
};
const fuzzyMatch = (query, target) => {
const lowerQuery = query.toLowerCase();
const lowerTarget = target.toLowerCase();
if (lowerQuery.length === 0) return null;
if (lowerTarget.length === 0) return null;
if (lowerTarget === lowerQuery) {
return 1.0;
}
if (lowerTarget.includes(lowerQuery)) {
const ratio = lowerQuery.length / lowerTarget.length;
return 0.8 + ratio * 0.2;
}
const match = findBestSubsequenceMatch(lowerQuery, lowerTarget);
if (!match) {
return null;
}
return Math.min(1.0, match.score);
};
self.onmessage = function (e) {
const { messageId, type, data } = e.data;
const respond = (type, data) => {
self.postMessage({ messageId, type, data });
};
const respondError = (error) => {
self.postMessage({
messageId,
type: "error",
error: error.message || String(error),
});
};
try {
if (type === "tokenize") {
const text = typeof data === "string" ? data : "";
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
const tokens = words.filter((word) => word.length > 2);
const uniqueTokens = Array.from(new Set(tokens));
respond("tokens", uniqueTokens);
} else if (type === "search") {
const { query, limit = 10 } = data;
if (!query || typeof query !== "string") {
respond("results", []);
return;
}
const rawQuery = query.toLowerCase();
const text = typeof query === "string" ? query : "";
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
const searchTerms = words.filter((word) => word.length > 2);
let documents = [];
if (typeof data.documents === "string") {
documents = JSON.parse(data.documents);
} else if (Array.isArray(data.documents)) {
documents = data.documents;
} else if (typeof data.transferables === "string") {
documents = JSON.parse(data.transferables);
}
if (!Array.isArray(documents) || documents.length === 0) {
respond("results", []);
return;
}
const useFuzzySearch = rawQuery.length >= 3;
if (searchTerms.length === 0 && rawQuery.length < 3) {
respond("results", []);
return;
}
const pageMatches = new Map();
// Pre-compute lower-case strings for each document
const processedDocs = documents.map((doc, docId) => {
const title = typeof doc.title === "string" ? doc.title : "";
const content = typeof doc.content === "string" ? doc.content : "";
return {
docId,
doc,
lowerTitle: title.toLowerCase(),
lowerContent: content.toLowerCase(),
};
});
// First pass: Score pages with fuzzy matching
processedDocs.forEach(({ docId, doc, lowerTitle, lowerContent }) => {
let match = pageMatches.get(docId);
if (!match) {
match = { doc, pageScore: 0, matchingAnchors: [] };
pageMatches.set(docId, match);
}
if (useFuzzySearch) {
const fuzzyTitleScore = fuzzyMatch(rawQuery, lowerTitle);
if (fuzzyTitleScore !== null) {
match.pageScore += fuzzyTitleScore * 100;
}
const fuzzyContentScore = fuzzyMatch(rawQuery, lowerContent);
if (fuzzyContentScore !== null) {
match.pageScore += fuzzyContentScore * 30;
}
}
// Token-based exact matching
searchTerms.forEach((term) => {
if (lowerTitle.includes(term)) {
match.pageScore += lowerTitle === term ? 20 : 10;
}
if (lowerContent.includes(term)) {
match.pageScore += 2;
}
});
});
// Second pass: Find matching anchors
pageMatches.forEach((match) => {
const doc = match.doc;
if (
!doc.anchors ||
!Array.isArray(doc.anchors) ||
doc.anchors.length === 0
) {
return;
}
doc.anchors.forEach((anchor) => {
if (!anchor || !anchor.text) return;
const anchorText = anchor.text.toLowerCase();
let anchorMatches = false;
if (useFuzzySearch) {
const fuzzyScore = fuzzyMatch(rawQuery, anchorText);
if (fuzzyScore !== null && fuzzyScore >= 0.4) {
anchorMatches = true;
}
}
if (!anchorMatches) {
searchTerms.forEach((term) => {
if (anchorText.includes(term)) {
anchorMatches = true;
}
});
}
if (anchorMatches) {
match.matchingAnchors.push(anchor);
}
});
});
const results = Array.from(pageMatches.values())
.filter((m) => m.pageScore > 5)
.sort((a, b) => b.pageScore - a.pageScore)
.slice(0, limit);
respond("results", results);
}
} catch (error) {
respondError(error);
}
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,153 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Known Issues and Quirks</title>
<script>
// Apply sidebar state immediately to prevent flash
(function () {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
})();
</script>
<link rel="stylesheet" href="assets/style.css" />
<script defer src="assets/main.js"></script>
<script>
window.searchNamespace = window.searchNamespace || {};
window.searchNamespace.rootPath = "";
</script>
<script defer src="assets/search.js"></script>
</head>
<body>
<div class="container">
<header>
<div class="header-left">
<h1 class="site-title">
<a href="index.html">NVF</a>
</h1>
<nav class="header-nav">
<ul>
<li >
<a href="options.html">Options</a>
</li>
<li><a href="search.html">Search</a></li>
</ul>
</nav>
</div>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search..." />
<div id="search-results" class="search-results"></div>
</div>
</header>
<div class="layout">
<div class="sidebar-toggle" aria-label="Toggle sidebar">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
</svg>
</div>
<nav class="sidebar">
<details class="sidebar-section" data-section="docs" open>
<summary>Documents</summary>
<div class="sidebar-section-content">
<ul>
<li><a href="index.html">Introduction</a></li>
<li><a href="configuring.html">Configuring nvf</a></li>
<li><a href="hacking.html">Hacking nvf</a></li>
<li><a href="tips.html">Helpful Tips</a></li>
<li><a href="quirks.html">Known Issues and Quirks</a></li>
<li><a href="release-notes.html">Release Notes</a></li>
<li><a href="search.html">Search</a></li>
</ul>
</div>
</details>
<details class="sidebar-section" data-section="toc" open>
<summary>Contents</summary>
<div class="sidebar-section-content">
<ul class="toc-list">
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
</ul><li><a href="#ch-bugs-suggestions">Bugs &amp; Suggestions</a>
</li></ul></li>
</ul>
</div>
</details>
</nav>
<main class="content"><html><head></head><body><h1 id="ch-known-issues-quirks">Known Issues and Quirks</h1>
<p>At times, certain plugins and modules may refuse to play nicely with your setup,
be it a result of generating Lua from Nix, or the state of packaging. This page,
in turn, will list any known modules or plugins that are known to misbehave, and
possible workarounds that you may apply.</p>
<h2 id="ch-quirks-nodejs">NodeJS</h2>
<h3 id="sec-eslint-plugin-prettier">eslint-plugin-prettier</h3>
<p>When working with NodeJS, which is <em>obviously</em> known for its meticulous
standards, most things are bound to work as expected but some projects, tools
and settings may fool the default configurations of tools provided by <strong>nvf</strong>.</p>
<p>If</p>
<p>If <a href="https://github.com/prettier/eslint-plugin-prettier">eslint-plugin-prettier</a> or similar is included, you might get a situation
where your Eslint configuration diagnoses your formatting according to its own
config (usually <code>.eslintrc.js</code>). The issue there is your formatting is made via
prettierd.</p>
<p>This results in auto-formatting relying on your prettier configuration, while
your Eslint configuration diagnoses formatting "issues" while it's
<a href="https://prettier.io/docs/en/comparison.html">not supposed to</a>. In the end, you get discrepancies between what your editor
does and what it wants.</p>
<p>Solutions are:</p>
<ol>
<li>Don't add a formatting config to Eslint, instead separate Prettier and
Eslint.</li>
<li>PR the repo in question to add an ESLint formatter, and configure <strong>nvf</strong> to
use it.</li>
</ol>
<h2 id="ch-bugs-suggestions">Bugs &amp; Suggestions</h2>
<p>Some quirks are not exactly quirks, but bugs in the module system. If you notice
any issues with <strong>nvf</strong>, or this documentation, then please consider reporting
them over at the <a href="https://github.com/notashelf/nvf/issues">issue tracker</a>. Issues tab, in addition to the
<a href="https://github.com/notashelf/nvf/discussions">discussions tab</a> is a good place as any to request new features.</p>
<p>You may also consider submitting bug fixes, feature additions and upstreamed
changes that you think are critical over at the <a href="https://github.com/notashelf/nvf/pulls">pull requests tab</a>.</p>
</body></html></main>
</div>
<aside class="page-toc">
<nav class="page-toc-nav">
<h3>On this page</h3>
<ul class="page-toc-list">
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
</ul><li><a href="#ch-bugs-suggestions">Bugs &amp; Suggestions</a>
</li></ul></li>
</ul>
</nav>
</aside>
<footer>
<p>Generated with ndg</p>
</footer>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,110 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NVF - Search</title>
<script>
// Apply sidebar state immediately to prevent flash
(function () {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
})();
</script>
<link rel="stylesheet" href="assets/style.css" />
<script defer src="assets/main.js"></script>
<script>
window.searchNamespace = window.searchNamespace || {};
window.searchNamespace.rootPath = "";
</script>
<script defer src="assets/search.js"></script>
</head>
<body>
<div class="container">
<header>
<div class="header-left">
<h1 class="site-title">
<a href="index.html">NVF</a>
</h1>
</div>
<nav class="header-nav">
<ul>
<li >
<a href="options.html">Options</a>
</li>
<li><a href="search.html">Search</a></li>
</ul>
</nav>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search..." />
<div id="search-results" class="search-results"></div>
</div>
</header>
<div class="layout">
<div class="sidebar-toggle" aria-label="Toggle sidebar">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
</svg>
</div>
<nav id="sidebar" class="sidebar">
<div class="docs-nav">
<h2>Documents</h2>
<ul>
<li><a href="index.html">Introduction</a></li>
<li><a href="configuring.html">Configuring nvf</a></li>
<li><a href="hacking.html">Hacking nvf</a></li>
<li><a href="tips.html">Helpful Tips</a></li>
<li><a href="quirks.html">Known Issues and Quirks</a></li>
<li><a href="release-notes.html">Release Notes</a></li>
<li><a href="search.html">Search</a></li>
</ul>
</div>
<div class="toc">
<h2>Contents</h2>
<ul class="toc-list">
</ul>
</div>
</nav>
<main class="content">
<h1>Search</h1>
<div class="search-page">
<div class="search-form">
<input
type="text"
id="search-page-input"
placeholder="Search..."
autofocus
/>
</div>
<div id="search-page-results" class="search-page-results"></div>
</div>
<div class="footnotes-container">
<!-- Footnotes will be appended here -->
</div>
</main>
</div>
<footer>
<p>Generated with ndg</p>
</footer>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long