mirror of
https://github.com/NotAShelf/neovim-flake.git
synced 2026-02-03 19:10:26 +01:00
deploy: 8050617656
This commit is contained in:
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
|
|
@ -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">×</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
|
|
@ -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
|
|
@ -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 & 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 & 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 & 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
|
|
@ -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
|
|
@ -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">×</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
|
|
@ -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
|
|
@ -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 & 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 & 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 & 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
|
|
@ -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
|
|
@ -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">×</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
|
|
@ -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
|
|
@ -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 & 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 & 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 & 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
|
|
@ -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
|
|
@ -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">×</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
|
|
@ -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
|
|
@ -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 & 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 & 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 & 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
|
|
@ -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
Loading…
Add table
Reference in a new issue