mirror of
https://github.com/NotAShelf/nvf.git
synced 2025-12-13 15:41:03 +00:00
Deploy PR #1281 preview
This commit is contained in:
parent
6df1b85124
commit
f10481a532
13 changed files with 55266 additions and 0 deletions
622
docs-preview-1281/assets/main.js
Normal file
622
docs-preview-1281/assets/main.js
Normal file
|
|
@ -0,0 +1,622 @@
|
||||||
|
// Polyfill for requestIdleCallback for Safari and unsupported browsers
|
||||||
|
if (typeof window.requestIdleCallback === "undefined") {
|
||||||
|
window.requestIdleCallback = function (cb) {
|
||||||
|
var start = Date.now();
|
||||||
|
var 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (e) {
|
||||||
|
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 mobileSidebarContent = document.querySelector(
|
||||||
|
".mobile-sidebar-content",
|
||||||
|
);
|
||||||
|
const mobileSidebarHandle = document.querySelector(".mobile-sidebar-handle");
|
||||||
|
const desktopSidebar = document.querySelector(".sidebar");
|
||||||
|
|
||||||
|
// Always set up FAB if it exists
|
||||||
|
if (mobileSidebarFab && mobileSidebarContainer) {
|
||||||
|
// Populate content if desktop sidebar exists
|
||||||
|
if (desktopSidebar && mobileSidebarContent) {
|
||||||
|
mobileSidebarContent.innerHTML = desktopSidebar.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
1
docs-preview-1281/assets/search-data.json
Normal file
1
docs-preview-1281/assets/search-data.json
Normal file
File diff suppressed because one or more lines are too long
64
docs-preview-1281/assets/search-worker.js
Normal file
64
docs-preview-1281/assets/search-worker.js
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
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 tokens = (typeof data === 'string' ? data : '')
|
||||||
|
.toLowerCase()
|
||||||
|
.match(/\b[a-zA-Z0-9_-]+\b/g) || []
|
||||||
|
.filter(word => word.length > 2);
|
||||||
|
|
||||||
|
const uniqueTokens = Array.from(new Set(tokens));
|
||||||
|
respond('tokens', uniqueTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'search') {
|
||||||
|
const { documents, query, limit } = data;
|
||||||
|
const searchTerms = (typeof query === 'string' ? query : '')
|
||||||
|
.toLowerCase()
|
||||||
|
.match(/\b[a-zA-Z0-9_-]+\b/g) || []
|
||||||
|
.filter(word => word.length > 2);
|
||||||
|
|
||||||
|
const docScores = new Map();
|
||||||
|
|
||||||
|
// Pre-compute lower-case terms once
|
||||||
|
const lowerSearchTerms = searchTerms.map(term => term.toLowerCase());
|
||||||
|
|
||||||
|
// Pre-compute lower-case strings for each document
|
||||||
|
const processedDocs = documents.map((doc, docId) => ({
|
||||||
|
docId,
|
||||||
|
title: doc.title,
|
||||||
|
content: doc.content,
|
||||||
|
lowerTitle: doc.title.toLowerCase(),
|
||||||
|
lowerContent: doc.content.toLowerCase()
|
||||||
|
}));
|
||||||
|
|
||||||
|
lowerSearchTerms.forEach(lowerTerm => {
|
||||||
|
processedDocs.forEach(({ docId, title, content, lowerTitle, lowerContent }) => {
|
||||||
|
if (lowerTitle.includes(lowerTerm) || lowerContent.includes(lowerTerm)) {
|
||||||
|
const score = lowerTitle === lowerTerm ? 30 :
|
||||||
|
lowerTitle.includes(lowerTerm) ? 10 : 2;
|
||||||
|
docScores.set(docId, (docScores.get(docId) || 0) + score);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = Array.from(docScores.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, limit)
|
||||||
|
.map(([docId, score]) => ({ ...documents[docId], score }));
|
||||||
|
|
||||||
|
respond('results', results);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
respondError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
786
docs-preview-1281/assets/search.js
Normal file
786
docs-preview-1281/assets/search.js
Normal file
|
|
@ -0,0 +1,786 @@
|
||||||
|
if (!window.searchNamespace) window.searchNamespace = {};
|
||||||
|
|
||||||
|
class SearchEngine {
|
||||||
|
constructor() {
|
||||||
|
this.documents = [];
|
||||||
|
this.tokenMap = new Map();
|
||||||
|
this.isLoaded = false;
|
||||||
|
this.loadError = false;
|
||||||
|
this.useWebWorker = typeof Worker !== 'undefined' && searchWorker !== null;
|
||||||
|
this.fullDocuments = null; // for lazy loading
|
||||||
|
this.rootPath = window.searchNamespace?.rootPath || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load search data from JSON
|
||||||
|
async loadData() {
|
||||||
|
if (this.isLoaded && !this.loadError) return;
|
||||||
|
|
||||||
|
// Clear previous error state on retry
|
||||||
|
this.loadError = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load JSON data, try multiple possible paths
|
||||||
|
// FIXME: There is only one possible path for now, and this search data is guaranteed
|
||||||
|
// to generate at this location, but we'll want to extend this in the future.
|
||||||
|
const possiblePaths = ["/assets/search-data.json"];
|
||||||
|
|
||||||
|
let response = null;
|
||||||
|
let usedPath = "";
|
||||||
|
|
||||||
|
for (const path of possiblePaths) {
|
||||||
|
try {
|
||||||
|
const testResponse = await fetch(path);
|
||||||
|
if (testResponse.ok) {
|
||||||
|
response = testResponse;
|
||||||
|
usedPath = path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue to next path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("Search data file not found at any expected location");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Loading search data from: ${usedPath}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use optimized JSON parsing for large files
|
||||||
|
const documents = await this.parseLargeJSON(response);
|
||||||
|
if (!Array.isArray(documents)) {
|
||||||
|
throw new Error("Invalid search data format");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initializeFromDocuments(documents);
|
||||||
|
this.isLoaded = true;
|
||||||
|
console.log(`Loaded ${documents.length} documents for search`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading search data:", error);
|
||||||
|
this.documents = [];
|
||||||
|
this.tokenMap.clear();
|
||||||
|
this.loadError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize from documents array
|
||||||
|
async initializeFromDocuments(documents) {
|
||||||
|
if (!Array.isArray(documents)) {
|
||||||
|
console.error("Invalid documents format:", typeof documents);
|
||||||
|
this.documents = [];
|
||||||
|
} else {
|
||||||
|
this.documents = documents;
|
||||||
|
console.log(`Initialized with ${documents.length} documents`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.buildTokenMap();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error building token map:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize from search index structure
|
||||||
|
initializeIndex(indexData) {
|
||||||
|
this.documents = indexData.documents || [];
|
||||||
|
this.tokenMap = new Map(Object.entries(indexData.tokenMap || {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build token map
|
||||||
|
// This is helpful for faster searching with progressive loading
|
||||||
|
buildTokenMap() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.tokenMap.clear();
|
||||||
|
|
||||||
|
if (!Array.isArray(this.documents)) {
|
||||||
|
console.error("No documents to build token map");
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDocs = this.documents.length;
|
||||||
|
let processedDocs = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Process in chunks to avoid blocking UI
|
||||||
|
const processChunk = (startIndex, chunkSize) => {
|
||||||
|
try {
|
||||||
|
const endIndex = Math.min(startIndex + chunkSize, totalDocs);
|
||||||
|
|
||||||
|
for (let i = startIndex; i < endIndex; i++) {
|
||||||
|
const doc = this.documents[i];
|
||||||
|
if (!doc || typeof doc.title !== 'string' || typeof doc.content !== 'string') {
|
||||||
|
console.warn(`Invalid document at index ${i}:`, doc);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = this.tokenize(doc.title + " " + doc.content);
|
||||||
|
tokens.forEach(token => {
|
||||||
|
if (!this.tokenMap.has(token)) {
|
||||||
|
this.tokenMap.set(token, []);
|
||||||
|
}
|
||||||
|
this.tokenMap.get(token).push(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
processedDocs++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress and yield control
|
||||||
|
if (endIndex < totalDocs) {
|
||||||
|
setTimeout(() => processChunk(endIndex, chunkSize), 0);
|
||||||
|
} else {
|
||||||
|
console.log(`Built token map with ${this.tokenMap.size} unique tokens from ${processedDocs} documents`);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start processing with small chunks
|
||||||
|
processChunk(0, 100);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokenize text into searchable terms
|
||||||
|
tokenize(text) {
|
||||||
|
const tokens = new Set();
|
||||||
|
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||||
|
|
||||||
|
words.forEach(word => {
|
||||||
|
if (word.length > 2) {
|
||||||
|
tokens.add(word);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advanced search with ranking
|
||||||
|
async search(query, limit = 10) {
|
||||||
|
if (!query.trim()) return [];
|
||||||
|
|
||||||
|
// Wait for data to be loaded
|
||||||
|
if (!this.isLoaded) {
|
||||||
|
await this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isLoaded || this.documents.length === 0) {
|
||||||
|
console.log("Search data not available");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTerms = this.tokenize(query);
|
||||||
|
if (searchTerms.length === 0) return [];
|
||||||
|
|
||||||
|
// Fallback to basic search if token map is empty
|
||||||
|
if (this.tokenMap.size === 0) {
|
||||||
|
return this.fallbackSearch(query, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Web Worker for large datasets to avoid blocking UI
|
||||||
|
if (this.useWebWorker && this.documents.length > 1000) {
|
||||||
|
return await this.searchWithWorker(query, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For very large datasets, implement lazy loading with candidate docIds
|
||||||
|
if (this.documents.length > 10000) {
|
||||||
|
const candidateDocIds = new Set();
|
||||||
|
searchTerms.forEach(term => {
|
||||||
|
const docIds = this.tokenMap.get(term) || [];
|
||||||
|
docIds.forEach(id => candidateDocIds.add(id));
|
||||||
|
});
|
||||||
|
const docIds = Array.from(candidateDocIds);
|
||||||
|
return await this.lazyLoadDocuments(docIds, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docScores = new Map();
|
||||||
|
|
||||||
|
searchTerms.forEach(term => {
|
||||||
|
const docIds = this.tokenMap.get(term) || [];
|
||||||
|
docIds.forEach(docId => {
|
||||||
|
const doc = this.documents[docId];
|
||||||
|
if (!doc) return;
|
||||||
|
|
||||||
|
const currentScore = docScores.get(docId) || 0;
|
||||||
|
|
||||||
|
// Calculate score based on term position and importance
|
||||||
|
let score = 1;
|
||||||
|
|
||||||
|
// Title matches get higher score
|
||||||
|
if (doc.title.toLowerCase().includes(term)) {
|
||||||
|
score += 10;
|
||||||
|
// Exact title match gets even higher score
|
||||||
|
if (doc.title.toLowerCase() === term) {
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content matches
|
||||||
|
if (doc.content.toLowerCase().includes(term)) {
|
||||||
|
score += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boost for multiple term matches
|
||||||
|
docScores.set(docId, currentScore + score);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by score and return top results
|
||||||
|
const scoredResults = Array.from(docScores.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
|
return scoredResults
|
||||||
|
.map(([docId, score]) => ({
|
||||||
|
...this.documents[docId],
|
||||||
|
score
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate search preview with highlighting
|
||||||
|
generatePreview(content, query, maxLength = 150) {
|
||||||
|
const lowerContent = content.toLowerCase();
|
||||||
|
|
||||||
|
let bestIndex = -1;
|
||||||
|
let bestScore = 0;
|
||||||
|
let bestMatch = "";
|
||||||
|
|
||||||
|
// Find the best match position
|
||||||
|
const queryWords = this.tokenize(query);
|
||||||
|
queryWords.forEach(word => {
|
||||||
|
const index = lowerContent.indexOf(word);
|
||||||
|
if (index !== -1) {
|
||||||
|
const score = word.length; // longer words get higher priority
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
bestIndex = index;
|
||||||
|
bestMatch = word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bestIndex === -1) {
|
||||||
|
return this.escapeHtml(content.slice(0, maxLength)) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Math.max(0, bestIndex - 50);
|
||||||
|
const end = Math.min(content.length, bestIndex + bestMatch.length + 50);
|
||||||
|
let preview = content.slice(start, end);
|
||||||
|
|
||||||
|
if (start > 0) preview = "..." + preview;
|
||||||
|
if (end < content.length) preview += "...";
|
||||||
|
|
||||||
|
// Escape HTML first, then highlight
|
||||||
|
preview = this.escapeHtml(preview);
|
||||||
|
preview = this.highlightTerms(preview, queryWords);
|
||||||
|
|
||||||
|
return preview;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape HTML to prevent XSS
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight search terms in text
|
||||||
|
highlightTerms(text, terms) {
|
||||||
|
let highlighted = text;
|
||||||
|
|
||||||
|
// Sort terms by length (longer first) to avoid overlapping highlights
|
||||||
|
const sortedTerms = [...terms].sort((a, b) => b.length - a.length);
|
||||||
|
|
||||||
|
sortedTerms.forEach(term => {
|
||||||
|
const regex = new RegExp(`(${this.escapeRegex(term)})`, 'gi');
|
||||||
|
highlighted = highlighted.replace(regex, '<mark>$1</mark>');
|
||||||
|
});
|
||||||
|
|
||||||
|
return highlighted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web Worker search for large datasets
|
||||||
|
async searchWithWorker(query, limit) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const messageId = `search_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error('Web Worker search timeout'));
|
||||||
|
}, 5000); // 5 second timeout
|
||||||
|
|
||||||
|
const handleMessage = (e) => {
|
||||||
|
if (e.data.messageId !== messageId) return;
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
if (e.data.type === 'results') {
|
||||||
|
resolve(e.data.data);
|
||||||
|
} else if (e.data.type === 'error') {
|
||||||
|
reject(new Error(e.data.error || 'Unknown worker error'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
cleanup();
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
searchWorker.removeEventListener('message', handleMessage);
|
||||||
|
searchWorker.removeEventListener('error', handleError);
|
||||||
|
};
|
||||||
|
|
||||||
|
searchWorker.addEventListener('message', handleMessage);
|
||||||
|
searchWorker.addEventListener('error', handleError);
|
||||||
|
|
||||||
|
searchWorker.postMessage({
|
||||||
|
messageId,
|
||||||
|
type: 'search',
|
||||||
|
data: { documents: this.documents, query, limit }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape regex special characters
|
||||||
|
escapeRegex(string) {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve path relative to current page location
|
||||||
|
resolvePath(path) {
|
||||||
|
// If path already starts with '/', it's absolute from domain root
|
||||||
|
if (path.startsWith('/')) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If path starts with '#', it's a fragment on current page
|
||||||
|
if (path.startsWith('#')) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepend root path for relative navigation
|
||||||
|
return this.rootPath + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimized JSON parser for large files
|
||||||
|
async parseLargeJSON(response) {
|
||||||
|
const contentLength = response.headers.get('content-length');
|
||||||
|
|
||||||
|
// For small files, use regular JSON parsing
|
||||||
|
if (!contentLength || parseInt(contentLength) < 1024 * 1024) { // < 1MB
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For large files, use streaming approach
|
||||||
|
console.log(`Large search file detected (${contentLength} bytes), using streaming parser`);
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// Process in chunks to avoid blocking main thread
|
||||||
|
if (buffer.length > 100 * 1024) { // 100KB chunks
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy loading for search results
|
||||||
|
async lazyLoadDocuments(docIds, limit = 10) {
|
||||||
|
if (!this.fullDocuments) {
|
||||||
|
// Store full documents separately for memory efficiency
|
||||||
|
this.fullDocuments = this.documents;
|
||||||
|
// Create lightweight index documents
|
||||||
|
this.documents = this.documents.map(doc => ({
|
||||||
|
id: doc.id,
|
||||||
|
title: doc.title,
|
||||||
|
path: doc.path
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return docIds.slice(0, limit).map(id => this.fullDocuments[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback search method (simple string matching)
|
||||||
|
fallbackSearch(query, limit = 10) {
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
const results = this.documents
|
||||||
|
.map(doc => {
|
||||||
|
const titleMatch = doc.title.toLowerCase().indexOf(lowerQuery);
|
||||||
|
const contentMatch = doc.content.toLowerCase().indexOf(lowerQuery);
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
if (titleMatch !== -1) {
|
||||||
|
score += 10;
|
||||||
|
if (doc.title.toLowerCase() === lowerQuery) {
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contentMatch !== -1) {
|
||||||
|
score += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { doc, score, titleMatch, contentMatch };
|
||||||
|
})
|
||||||
|
.filter(item => item.score > 0)
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.score !== b.score) return b.score - a.score;
|
||||||
|
if (a.titleMatch !== b.titleMatch) return a.titleMatch - b.titleMatch;
|
||||||
|
return a.contentMatch - b.contentMatch;
|
||||||
|
})
|
||||||
|
.slice(0, limit)
|
||||||
|
.map(item => ({ ...item.doc, score: item.score }));
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web Worker for background search processing
|
||||||
|
// This is CLEARLY the best way to do it lmao.
|
||||||
|
// Create Web Worker if supported
|
||||||
|
let searchWorker = null;
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
try {
|
||||||
|
searchWorker = new Worker('/assets/search-worker.js');
|
||||||
|
console.log('Web Worker initialized for background search');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Web Worker creation failed, using main thread:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global search engine instance
|
||||||
|
window.searchNamespace.engine = new SearchEngine();
|
||||||
|
|
||||||
|
// Mobile search timeout for debouncing
|
||||||
|
let mobileSearchTimeout = null;
|
||||||
|
|
||||||
|
// Legacy search for backward compatibility
|
||||||
|
// This could be removed, but I'm emotionally attached to it
|
||||||
|
// and it could be used as a fallback.
|
||||||
|
function filterSearchResults(data, searchTerm, limit = 10) {
|
||||||
|
return data
|
||||||
|
.filter(
|
||||||
|
(doc) =>
|
||||||
|
doc.title.toLowerCase().includes(searchTerm) ||
|
||||||
|
doc.content.toLowerCase().includes(searchTerm),
|
||||||
|
)
|
||||||
|
.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
// Initialize search engine immediately
|
||||||
|
window.searchNamespace.engine.loadData().then(() => {
|
||||||
|
console.log("Search data loaded successfully");
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Failed to initialize search:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search page specific functionality
|
||||||
|
const searchPageInput = document.getElementById("search-page-input");
|
||||||
|
if (searchPageInput) {
|
||||||
|
// Set up event listener
|
||||||
|
searchPageInput.addEventListener("input", function() {
|
||||||
|
performSearch(this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Perform search if URL has query
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const query = params.get("q");
|
||||||
|
if (query) {
|
||||||
|
searchPageInput.value = query;
|
||||||
|
performSearch(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop Sidebar Toggle
|
||||||
|
const searchInput = document.getElementById("search-input");
|
||||||
|
if (searchInput) {
|
||||||
|
const searchResults = document.getElementById("search-results");
|
||||||
|
|
||||||
|
searchInput.addEventListener("input", async function() {
|
||||||
|
const searchTerm = this.value.trim();
|
||||||
|
|
||||||
|
if (searchTerm.length < 2) {
|
||||||
|
searchResults.innerHTML = "";
|
||||||
|
searchResults.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
searchResults.innerHTML = '<div class="search-result-item">Loading...</div>';
|
||||||
|
searchResults.style.display = "block";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await window.searchNamespace.engine.search(searchTerm, 8);
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
searchResults.innerHTML = results
|
||||||
|
.map(
|
||||||
|
(doc) => {
|
||||||
|
const highlightedTitle = window.searchNamespace.engine.highlightTerms(
|
||||||
|
doc.title,
|
||||||
|
window.searchNamespace.engine.tokenize(searchTerm)
|
||||||
|
);
|
||||||
|
const resolvedPath = window.searchNamespace.engine.resolvePath(doc.path);
|
||||||
|
return `
|
||||||
|
<div class="search-result-item">
|
||||||
|
<a href="${resolvedPath}">${highlightedTitle}</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
searchResults.style.display = "block";
|
||||||
|
} else {
|
||||||
|
searchResults.innerHTML =
|
||||||
|
'<div class="search-result-item">No results found</div>';
|
||||||
|
searchResults.style.display = "block";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Search error:", error);
|
||||||
|
searchResults.innerHTML =
|
||||||
|
'<div class="search-result-item">Search unavailable</div>';
|
||||||
|
searchResults.style.display = "block";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide results when clicking outside
|
||||||
|
document.addEventListener("click", function(event) {
|
||||||
|
if (
|
||||||
|
!searchInput.contains(event.target) &&
|
||||||
|
!searchResults.contains(event.target)
|
||||||
|
) {
|
||||||
|
searchResults.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus search when pressing slash key
|
||||||
|
document.addEventListener("keydown", function(event) {
|
||||||
|
if (event.key === "/" && document.activeElement !== searchInput) {
|
||||||
|
event.preventDefault();
|
||||||
|
searchInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile search functionality
|
||||||
|
// This detects mobile viewport and adds click behavior
|
||||||
|
function isMobile() {
|
||||||
|
return window.innerWidth <= 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchInput) {
|
||||||
|
// Add mobile search behavior
|
||||||
|
searchInput.addEventListener("click", function(e) {
|
||||||
|
if (isMobile()) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
openMobileSearch();
|
||||||
|
}
|
||||||
|
// On desktop, let the normal click behavior work (focus the input)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent typing on mobile (input should only open popup)
|
||||||
|
searchInput.addEventListener("keydown", function(e) {
|
||||||
|
if (isMobile()) {
|
||||||
|
e.preventDefault();
|
||||||
|
openMobileSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile search popup functionality
|
||||||
|
let mobileSearchPopup = document.getElementById("mobile-search-popup");
|
||||||
|
let mobileSearchInput = document.getElementById("mobile-search-input");
|
||||||
|
let mobileSearchResults = document.getElementById("mobile-search-results");
|
||||||
|
const closeMobileSearchBtn = document.getElementById("close-mobile-search");
|
||||||
|
|
||||||
|
function openMobileSearch() {
|
||||||
|
if (mobileSearchPopup) {
|
||||||
|
mobileSearchPopup.classList.add("active");
|
||||||
|
// Focus the input after a small delay to ensure the popup is visible
|
||||||
|
setTimeout(() => {
|
||||||
|
if (mobileSearchInput) {
|
||||||
|
mobileSearchInput.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMobileSearch() {
|
||||||
|
if (mobileSearchPopup) {
|
||||||
|
mobileSearchPopup.classList.remove("active");
|
||||||
|
if (mobileSearchInput) {
|
||||||
|
mobileSearchInput.value = "";
|
||||||
|
}
|
||||||
|
if (mobileSearchResults) {
|
||||||
|
mobileSearchResults.innerHTML = "";
|
||||||
|
mobileSearchResults.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeMobileSearchBtn) {
|
||||||
|
closeMobileSearchBtn.addEventListener("click", closeMobileSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close mobile search when clicking outside
|
||||||
|
document.addEventListener("click", function(event) {
|
||||||
|
if (
|
||||||
|
mobileSearchPopup &&
|
||||||
|
mobileSearchPopup.classList.contains("active") &&
|
||||||
|
!mobileSearchPopup.contains(event.target) &&
|
||||||
|
!searchInput.contains(event.target)
|
||||||
|
) {
|
||||||
|
closeMobileSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close mobile search on escape key
|
||||||
|
document.addEventListener("keydown", function(event) {
|
||||||
|
if (
|
||||||
|
event.key === "Escape" &&
|
||||||
|
mobileSearchPopup &&
|
||||||
|
mobileSearchPopup.classList.contains("active")
|
||||||
|
) {
|
||||||
|
closeMobileSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mobile search input
|
||||||
|
if (mobileSearchInput && mobileSearchResults) {
|
||||||
|
function handleMobileSearchInput() {
|
||||||
|
clearTimeout(mobileSearchTimeout);
|
||||||
|
const searchTerm = mobileSearchInput.value.trim();
|
||||||
|
if (searchTerm.length < 2) {
|
||||||
|
mobileSearchResults.innerHTML = "";
|
||||||
|
mobileSearchResults.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mobileSearchTimeout = setTimeout(async () => {
|
||||||
|
// Verify the input still matches before proceeding
|
||||||
|
if (mobileSearchInput.value.trim() !== searchTerm) return;
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
mobileSearchResults.innerHTML = '<div class="search-result-item">Loading...</div>';
|
||||||
|
mobileSearchResults.style.display = "block";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await window.searchNamespace.engine.search(searchTerm, 8);
|
||||||
|
// Verify again after async operation
|
||||||
|
if (mobileSearchInput.value.trim() !== searchTerm) return;
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
mobileSearchResults.innerHTML = results
|
||||||
|
.map(
|
||||||
|
(doc) => {
|
||||||
|
const highlightedTitle = window.searchNamespace.engine.highlightTerms(
|
||||||
|
doc.title,
|
||||||
|
window.searchNamespace.engine.tokenize(searchTerm)
|
||||||
|
);
|
||||||
|
const resolvedPath = window.searchNamespace.engine.resolvePath(doc.path);
|
||||||
|
return `
|
||||||
|
<div class="search-result-item">
|
||||||
|
<a href="${resolvedPath}">${highlightedTitle}</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
mobileSearchResults.style.display = "block";
|
||||||
|
} else {
|
||||||
|
mobileSearchResults.innerHTML =
|
||||||
|
'<div class="search-result-item">No results found</div>';
|
||||||
|
mobileSearchResults.style.display = "block";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Mobile search error:", error);
|
||||||
|
// Verify once more
|
||||||
|
if (mobileSearchInput.value.trim() !== searchTerm) return;
|
||||||
|
mobileSearchResults.innerHTML =
|
||||||
|
'<div class="search-result-item">Search unavailable</div>';
|
||||||
|
mobileSearchResults.style.display = "block";
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
mobileSearchInput.addEventListener("input", handleMobileSearchInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle window resize to update mobile behavior
|
||||||
|
window.addEventListener("resize", function() {
|
||||||
|
// Close mobile search if window is resized to desktop size
|
||||||
|
if (
|
||||||
|
!isMobile() &&
|
||||||
|
mobileSearchPopup &&
|
||||||
|
mobileSearchPopup.classList.contains("active")
|
||||||
|
) {
|
||||||
|
closeMobileSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function performSearch(query) {
|
||||||
|
query = query.trim();
|
||||||
|
const resultsContainer = document.getElementById("search-page-results");
|
||||||
|
|
||||||
|
if (query.length < 2) {
|
||||||
|
resultsContainer.innerHTML =
|
||||||
|
"<p>Please enter at least 2 characters to search</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
resultsContainer.innerHTML = "<p>Searching...</p>";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await window.searchNamespace.engine.search(query, 50);
|
||||||
|
|
||||||
|
// Display results
|
||||||
|
if (results.length > 0) {
|
||||||
|
let html = '<ul class="search-results-list">';
|
||||||
|
const queryTerms = window.searchNamespace.engine.tokenize(query);
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const highlightedTitle = window.searchNamespace.engine.highlightTerms(result.title, queryTerms);
|
||||||
|
const preview = window.searchNamespace.engine.generatePreview(result.content, query);
|
||||||
|
const resolvedPath = window.searchNamespace.engine.resolvePath(result.path);
|
||||||
|
html += `<li class="search-result-item">
|
||||||
|
<a href="${resolvedPath}">
|
||||||
|
<div class="search-result-title">${highlightedTitle}</div>
|
||||||
|
<div class="search-result-preview">${preview}</div>
|
||||||
|
</a>
|
||||||
|
</li>`;
|
||||||
|
}
|
||||||
|
html += "</ul>";
|
||||||
|
resultsContainer.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
resultsContainer.innerHTML = "<p>No results found</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update URL with query
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set("q", query);
|
||||||
|
window.history.replaceState({}, "", url.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Search error:", error);
|
||||||
|
resultsContainer.innerHTML = "<p>Search temporarily unavailable</p>";
|
||||||
|
}
|
||||||
|
}
|
||||||
1945
docs-preview-1281/assets/style.css
Normal file
1945
docs-preview-1281/assets/style.css
Normal file
File diff suppressed because it is too large
Load diff
597
docs-preview-1281/configuring.html
Normal file
597
docs-preview-1281/configuring.html
Normal file
File diff suppressed because one or more lines are too long
386
docs-preview-1281/hacking.html
Normal file
386
docs-preview-1281/hacking.html
Normal file
File diff suppressed because one or more lines are too long
357
docs-preview-1281/index.html
Normal file
357
docs-preview-1281/index.html
Normal file
File diff suppressed because one or more lines are too long
48228
docs-preview-1281/options.html
Normal file
48228
docs-preview-1281/options.html
Normal file
File diff suppressed because it is too large
Load diff
133
docs-preview-1281/quirks.html
Normal file
133
docs-preview-1281/quirks.html
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
<!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">
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</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, everything works as expected, but some projects have
|
||||||
|
settings that can fool nvf.</p>
|
||||||
|
<p>If <a href="https://github.com/prettier/eslint-plugin-prettier">this plugin</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>).</p>
|
||||||
|
<p>The issue there is your formatting is made via prettierd.</p>
|
||||||
|
<p>This results in auto-formatting relying on your prettier config, while your
|
||||||
|
eslint config diagnoses formatting
|
||||||
|
<a href="https://prettier.io/docs/en/comparison.html">which it's not supposed to</a>)</p>
|
||||||
|
<p>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, and separate prettier and eslint.</li>
|
||||||
|
<li>PR this repo to add an ESLint formatter and configure nvf 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 systeme. If you
|
||||||
|
notice any issues with nvf, 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 bugfixes, 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>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>Generated with ndg</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1827
docs-preview-1281/release-notes.html
Normal file
1827
docs-preview-1281/release-notes.html
Normal file
File diff suppressed because one or more lines are too long
106
docs-preview-1281/search.html
Normal file
106
docs-preview-1281/search.html
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
<!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 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>
|
||||||
214
docs-preview-1281/tips.html
Normal file
214
docs-preview-1281/tips.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue