This commit is contained in:
NotAShelf 2026-04-22 21:19:14 +00:00
commit d39bd7cecd
64 changed files with 8748 additions and 249256 deletions

View file

@ -61,13 +61,16 @@ function createMobileElements() {
const mobileSearchPopup = document.createElement("div");
mobileSearchPopup.id = "mobile-search-popup";
mobileSearchPopup.className = "mobile-search-popup";
mobileSearchPopup.setAttribute("role", "dialog");
mobileSearchPopup.setAttribute("aria-modal", "true");
mobileSearchPopup.setAttribute("aria-label", "Search");
mobileSearchPopup.innerHTML = `
<div class="mobile-search-container">
<div class="mobile-search-container" role="document">
<div class="mobile-search-header">
<input type="text" id="mobile-search-input" placeholder="Search..." />
<button id="close-mobile-search" class="close-mobile-search" aria-label="Close search">&times;</button>
<input type="search" id="mobile-search-input" placeholder="Search..." aria-label="Search" autocomplete="off" />
<button type="button" id="close-mobile-search" class="close-mobile-search" aria-label="Close search">&times;</button>
</div>
<div id="mobile-search-results" class="mobile-search-results"></div>
<div id="mobile-search-results" class="mobile-search-results" role="region" aria-live="polite" aria-label="Search results"></div>
</div>
`;
@ -85,40 +88,51 @@ function createMobileElements() {
}
}
// 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",
);
// Highlight search terms on target pages
function highlightTextInContent(container, terms) {
if (!container || !terms || terms.length === 0) return;
sections.forEach((section) => {
const sectionId = section.dataset.section;
if (!sectionId) return;
// Create a case-insensitive regex pattern
const pattern = terms
.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
.join("|");
const regex = new RegExp(`(${pattern})`, "gi");
const storageKey = `sidebar-section-${sectionId}`;
const savedState = localStorage.getItem(storageKey);
// Elements to skip highlighting
const skipTags = new Set(["SCRIPT", "STYLE", "CODE", "PRE", "MARK"]);
// Restore saved state (default is open)
if (savedState === "closed") {
section.removeAttribute("open");
function highlightNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
// Use match instead of test to avoid regex state issues
if (text.match(regex)) {
const span = document.createElement("span");
// Create a fresh regex for replace to avoid state issues
const replaceRegex = new RegExp(`(${pattern})`, "gi");
span.innerHTML = text.replace(
replaceRegex,
'<mark class="search-highlight">$1</mark>',
);
node.replaceWith(...Array.from(span.childNodes));
}
} else if (
node.nodeType === Node.ELEMENT_NODE &&
!skipTags.has(node.tagName)
) {
Array.from(node.childNodes).forEach(highlightNode);
}
}
// Save state on toggle and sync between desktop/mobile
section.addEventListener("toggle", () => {
localStorage.setItem(storageKey, section.open ? "open" : "closed");
highlightNode(container);
// 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;
}
});
});
});
// Scroll to first highlight after a brief delay
setTimeout(() => {
const firstHighlight = container.querySelector(".search-highlight");
if (firstHighlight) {
firstHighlight.scrollIntoView({ behavior: "smooth", block: "center" });
firstHighlight.classList.add("search-highlight-active");
}
}, 100);
}
// Initialize scroll spy
@ -208,22 +222,100 @@ function initScrollSpy() {
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");
try {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
document.body.classList.add("sidebar-collapsed");
}
} catch {
// localStorage unavailable
}
if (!document.querySelector(".mobile-sidebar-fab")) {
createMobileElements();
}
// Initialize collapsible sidebar sections
// after mobile elements are created
initCollapsibleSections();
// Initialize scroll spy for page TOC
initScrollSpy();
// Template container for collapsed sidebar content (prevents Ctrl+F from finding hidden content)
const sidebarHiddenContainer = document.createElement("template");
// Handle sidebar section toggles - move content to template when collapsed
document
.querySelectorAll(".sidebar-section > .sidebar-section-content")
.forEach((content) => {
const details = content.parentElement;
const toggleContent = () => {
if (details.hasAttribute("open")) {
// Section opened - move content back to DOM
if (sidebarHiddenContainer.content.contains(content)) {
const summary = details.querySelector("summary");
details.insertBefore(
content,
summary ? summary.nextSibling : details.firstChild,
);
}
} else {
// Section closed - move content to template (removes from DOM, Ctrl+F won't find it)
if (content.parentElement === details) {
sidebarHiddenContainer.content.appendChild(content);
}
}
};
// Use MutationObserver to detect open/close changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "open") {
toggleContent();
}
});
});
observer.observe(details, { attributes: true });
// Initial state check
if (!details.hasAttribute("open")) {
sidebarHiddenContainer.content.appendChild(content);
}
});
// Handle sidebar collapse/expand - move entire sidebar to template when collapsed
const sidebar = document.querySelector(".sidebar");
const sidebarObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "class") {
const isCollapsed =
document.documentElement.classList.contains("sidebar-collapsed");
if (isCollapsed) {
// Sidebar collapsed - move to template
if (sidebar.parentElement) {
sidebarHiddenContainer.content.appendChild(sidebar);
}
} else {
// Sidebar expanded - move back to DOM
if (sidebarHiddenContainer.content.contains(sidebar)) {
const layout = document.querySelector(".layout");
const contentEl = document.querySelector(".content");
if (layout) {
layout.insertBefore(sidebar, contentEl);
}
}
}
}
});
});
if (sidebar) {
sidebarObserver.observe(document.documentElement, { attributes: true });
// Initial state - if collapsed, move sidebar to template
if (document.documentElement.classList.contains("sidebar-collapsed")) {
sidebarHiddenContainer.content.appendChild(sidebar);
}
}
// Desktop Sidebar Toggle
const sidebarToggle = document.querySelector(".sidebar-toggle");
@ -239,10 +331,13 @@ document.addEventListener("DOMContentLoaded", function () {
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);
const isCollapsed =
document.documentElement.classList.contains("sidebar-collapsed");
try {
localStorage.setItem("sidebar-collapsed", isCollapsed);
} catch {
// localStorage unavailable
}
});
}
@ -505,13 +600,10 @@ document.addEventListener("DOMContentLoaded", function () {
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);
}
// Template container for hidden options
const hiddenOptionsContainer = document.createElement("template");
hiddenOptionsContainer.id = "hidden-options-container";
document.body.appendChild(hiddenOptionsContainer);
// Create filter results counter
const filterResults = document.createElement("div");
@ -522,8 +614,8 @@ document.addEventListener("DOMContentLoaded", function () {
);
// Detect if we're on a mobile device
const isMobile = window.innerWidth < 768 ||
/Mobi|Android/i.test(navigator.userAgent);
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"));
@ -580,29 +672,26 @@ document.addEventListener("DOMContentLoaded", function () {
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
if (startIdx < itemsToProcess.length) {
// Process current chunk
// Move visible items to container, hide others
for (let i = startIdx; i < endIdx; i++) {
const item = itemsToProcess[i];
if (item.visible) {
item.element.classList.remove("option-hidden");
optionsContainer.appendChild(item.element);
} else {
item.element.classList.add("option-hidden");
hiddenOptionsContainer.content.appendChild(item.element);
}
}
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.textContent = `Showing ${filterResults.visibleCount} of ${totalCount} options`;
filterResults.style.display = "block";
} else {
filterResults.style.display = "none";
@ -611,9 +700,17 @@ document.addEventListener("DOMContentLoaded", function () {
}
}
// Initialize: keep all options visible by default
// They will be moved to hidden container only when filtering
function filterOptions() {
const searchTerm = optionsFilter.value.toLowerCase().trim();
// Skip if search term hasn't changed
if (filterOptions.lastTerm === searchTerm) {
return;
}
filterOptions.lastTerm = searchTerm;
if (pendingRender) {
cancelAnimationFrame(pendingRender);
pendingRender = null;
@ -622,12 +719,14 @@ document.addEventListener("DOMContentLoaded", function () {
itemsToProcess = [];
if (searchTerm === "") {
// Restore original DOM order when filter is cleared
// Restore to original order
const fragment = document.createDocumentFragment();
originalOptionOrder.forEach((option) => {
option.classList.remove("option-hidden");
fragment.appendChild(option);
hiddenOptionsContainer.content.appendChild(option);
});
while (hiddenOptionsContainer.content.firstChild) {
fragment.appendChild(hiddenOptionsContainer.content.firstChild);
}
optionsContainer.appendChild(fragment);
filterResults.style.display = "none";
return;
@ -640,55 +739,51 @@ document.addEventListener("DOMContentLoaded", function () {
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));
}
const term = searchTerms[0];
for (let i = 0; i < optionsData.length; i++) {
const data = optionsData[i];
const isTitleMatch = data.name.includes(term);
const isDescMatch = !isTitleMatch && data.description.includes(term);
if (isTitleMatch) {
visibleCount++;
titleMatches.push(data);
} else if (isDescMatch) {
visibleCount++;
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),
);
}
titleMatches.sort((a, b) => a.name.indexOf(term) - b.name.indexOf(term));
descMatches.sort(
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
);
const visibleElements = new Set();
itemsToProcess = [];
titleMatches.forEach((data) => {
visibleCount++;
for (let i = 0; i < titleMatches.length; i++) {
const data = titleMatches[i];
visibleElements.add(data.element);
itemsToProcess.push({ element: data.element, visible: true });
});
descMatches.forEach((data) => {
visibleCount++;
}
for (let i = 0; i < descMatches.length; i++) {
const data = descMatches[i];
visibleElements.add(data.element);
itemsToProcess.push({ element: data.element, visible: true });
});
optionsData.forEach((data) => {
if (!itemsToProcess.some((item) => item.element === data.element)) {
}
for (let i = 0; i < optionsData.length; i++) {
const data = optionsData[i];
if (!visibleElements.has(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);
});
for (let i = 0; i < itemsToProcess.length; i++) {
fragment.appendChild(itemsToProcess[i].element);
}
optionsContainer.appendChild(fragment);
filterResults.visibleCount = visibleCount;
@ -700,7 +795,6 @@ document.addEventListener("DOMContentLoaded", function () {
// Set up event listeners
optionsFilter.addEventListener("input", debouncedFilter);
optionsFilter.addEventListener("change", filterOptions);
// Allow clearing with Escape key
optionsFilter.addEventListener("keydown", function (e) {
@ -717,7 +811,7 @@ document.addEventListener("DOMContentLoaded", function () {
}
});
// Initially trigger filter if there's a value
// Run initial filter if there's a value
if (optionsFilter.value) {
filterOptions();
}
@ -737,4 +831,232 @@ document.addEventListener("DOMContentLoaded", function () {
});
}
}
// Lib filter functionality
const libFilter = document.getElementById("lib-filter");
if (libFilter && document.querySelector(".lib-container")) {
const libContainer = document.querySelector(".lib-container");
const hiddenLibContainer = document.createElement("template");
hiddenLibContainer.id = "hidden-lib-container";
document.body.appendChild(hiddenLibContainer);
const filterResults = document.createElement("div");
filterResults.className = "filter-results";
libFilter.parentNode.insertBefore(filterResults, libFilter.nextSibling);
const isMobile =
window.innerWidth < 768 || /Mobi|Android/i.test(navigator.userAgent);
const libEntries = Array.from(document.querySelectorAll(".lib-entry"));
const totalCount = libEntries.length;
const originalLibOrder = libEntries.slice();
const libData = libEntries.map((entry) => {
const nameElem = entry.querySelector(".lib-entry-name");
const descriptionElem = entry.querySelector(".lib-entry-description");
const id = entry.id ? entry.id.toLowerCase() : "";
const name = nameElem ? nameElem.textContent.toLowerCase() : "";
const description = descriptionElem
? descriptionElem.textContent.toLowerCase()
: "";
const keywords = (id + " " + name + " " + description)
.toLowerCase()
.split(/\s+/)
.filter((word) => word.length > 1);
return {
element: entry,
id,
name,
description,
keywords,
searchText: (id + " " + name + " " + description).toLowerCase(),
};
});
const CHUNK_SIZE = isMobile ? 15 : 40;
let pendingRender = null;
let currentChunk = 0;
let itemsToProcess = [];
function debounceLib(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
function processNextChunkLib() {
const startIdx = currentChunk * CHUNK_SIZE;
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
if (startIdx < itemsToProcess.length) {
for (let i = startIdx; i < endIdx; i++) {
const item = itemsToProcess[i];
if (item.visible) {
libContainer.appendChild(item.element);
} else {
hiddenLibContainer.content.appendChild(item.element);
}
}
currentChunk++;
pendingRender = requestAnimationFrame(processNextChunkLib);
} else {
pendingRender = null;
currentChunk = 0;
itemsToProcess = [];
if (filterResults.visibleCount !== undefined) {
if (filterResults.visibleCount < totalCount) {
filterResults.textContent = `Showing ${filterResults.visibleCount} of ${totalCount} functions`;
filterResults.style.display = "block";
} else {
filterResults.style.display = "none";
}
}
}
}
function filterLib() {
const searchTerm = libFilter.value.toLowerCase().trim();
if (filterLib.lastTerm === searchTerm) {
return;
}
filterLib.lastTerm = searchTerm;
if (pendingRender) {
cancelAnimationFrame(pendingRender);
pendingRender = null;
}
currentChunk = 0;
itemsToProcess = [];
if (searchTerm === "") {
const fragment = document.createDocumentFragment();
originalLibOrder.forEach((entry) => {
hiddenLibContainer.content.appendChild(entry);
});
while (hiddenLibContainer.content.firstChild) {
fragment.appendChild(hiddenLibContainer.content.firstChild);
}
libContainer.appendChild(fragment);
filterResults.style.display = "none";
return;
}
const searchTerms = searchTerm
.split(/\s+/)
.filter((term) => term.length > 0);
let visibleCount = 0;
const titleMatches = [];
const descMatches = [];
const term = searchTerms[0];
for (let i = 0; i < libData.length; i++) {
const data = libData[i];
const isTitleMatch = data.name.includes(term);
const isDescMatch = !isTitleMatch && data.description.includes(term);
if (isTitleMatch) {
visibleCount++;
titleMatches.push(data);
} else if (isDescMatch) {
visibleCount++;
descMatches.push(data);
}
}
titleMatches.sort((a, b) => a.name.indexOf(term) - b.name.indexOf(term));
descMatches.sort(
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
);
const visibleElements = new Set();
itemsToProcess = [];
for (let i = 0; i < titleMatches.length; i++) {
const data = titleMatches[i];
visibleElements.add(data.element);
itemsToProcess.push({ element: data.element, visible: true });
}
for (let i = 0; i < descMatches.length; i++) {
const data = descMatches[i];
visibleElements.add(data.element);
itemsToProcess.push({ element: data.element, visible: true });
}
for (let i = 0; i < libData.length; i++) {
const data = libData[i];
if (!visibleElements.has(data.element)) {
itemsToProcess.push({ element: data.element, visible: false });
}
}
const fragment = document.createDocumentFragment();
for (let i = 0; i < itemsToProcess.length; i++) {
fragment.appendChild(itemsToProcess[i].element);
}
libContainer.appendChild(fragment);
filterResults.visibleCount = visibleCount;
pendingRender = requestAnimationFrame(processNextChunkLib);
}
const debouncedFilter = debounceLib(filterLib, isMobile ? 200 : 100);
libFilter.addEventListener("input", debouncedFilter);
libFilter.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
libFilter.value = "";
filterLib();
}
});
document.addEventListener("visibilitychange", function () {
if (!document.hidden && libFilter.value) {
filterLib();
}
});
if (libFilter.value) {
filterLib();
}
if (isMobile && totalCount > 50) {
requestIdleCallback(() => {
const sampleEntry = libEntries[0];
if (sampleEntry) {
const height = sampleEntry.offsetHeight;
if (height > 0) {
libEntries.forEach((entry) => {
entry.style.containIntrinsicSize = `0 ${height}px`;
});
}
}
});
}
}
// URL-based search highlighting
const urlParams = new URLSearchParams(window.location.search);
const highlightQuery = urlParams.get("highlight");
if (highlightQuery && content) {
// Simple tokenizer that doesn't depend on search engine
const queryTerms = highlightQuery
.toLowerCase()
.trim()
.split(/\s+/)
.filter((term) => term.length >= 2); // min 2 chars like search engine
if (queryTerms.length > 0) {
highlightTextInContent(content, queryTerms);
}
}
});

File diff suppressed because one or more lines are too long

View file

@ -1,13 +1,25 @@
if (!window.searchNamespace) window.searchNamespace = {};
class SearchEngine {
// Characters to strip from search term ends for better matching
static STRIP_TRAILING_CHARS_RE = /[.,!?;:'"…—–-]+$/g;
constructor() {
this.documents = [];
this.tokenMap = new Map();
this.lowercaseCache = [];
this.isLoaded = false;
this.loadError = false;
this.fullDocuments = null; // for lazy loading
this.rootPath = window.searchNamespace?.rootPath || "";
// Search configuration (loaded from search data)
this.config = {
minWordLength: 2,
stopwords: [],
boostTitle: 100.0,
boostContent: 30.0,
boostAnchor: 10.0,
};
}
// Check if we can use Web Worker
@ -53,19 +65,30 @@ class SearchEngine {
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}`);
}
const documents = await response.json();
if (!Array.isArray(documents)) {
throw new Error("Invalid search data format");
// New format with config
if (documents.documents && Array.isArray(documents.documents)) {
this.config = {
minWordLength: documents.min_word_length || 2,
stopwords: documents.stopwords || [],
boostTitle: documents.boost_title || 100.0,
boostContent: documents.boost_content || 30.0,
boostAnchor: documents.boost_anchor || 10.0,
};
this.initializeFromDocuments(documents.documents);
} else {
throw new Error("Invalid search data format");
}
} else {
// Legacy format - just an array of documents
this.initializeFromDocuments(documents);
}
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 = [];
@ -81,7 +104,6 @@ class SearchEngine {
this.documents = [];
} else {
this.documents = documents;
console.log(`Initialized with ${documents.length} documents`);
}
try {
await this.buildTokenMap();
@ -94,10 +116,13 @@ class SearchEngine {
initializeIndex(indexData) {
this.documents = indexData.documents || [];
this.tokenMap = new Map(Object.entries(indexData.tokenMap || {}));
this.lowercaseCache = this.documents.map((doc) => ({
title: (doc.title || "").toLowerCase(),
content: (doc.content || "").toLowerCase(),
}));
}
// Build token map
// This is helpful for faster searching with progressive loading
// Build token map for faster searching
buildTokenMap() {
return new Promise((resolve, reject) => {
this.tokenMap.clear();
@ -111,6 +136,8 @@ class SearchEngine {
const totalDocs = this.documents.length;
let processedDocs = 0;
this.lowercaseCache = [];
try {
// Process in chunks to avoid blocking UI
const processChunk = (startIndex, chunkSize) => {
@ -128,7 +155,14 @@ class SearchEngine {
continue;
}
const tokens = this.tokenize(doc.title + " " + doc.content);
const lowerTitle = doc.title.toLowerCase();
const lowerContent = doc.content.toLowerCase();
this.lowercaseCache[i] = {
title: lowerTitle,
content: lowerContent,
};
const tokens = this.tokenize(lowerTitle + " " + lowerContent);
tokens.forEach((token) => {
if (!this.tokenMap.has(token)) {
this.tokenMap.set(token, []);
@ -143,9 +177,6 @@ class SearchEngine {
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) {
@ -308,8 +339,8 @@ class SearchEngine {
}
score = Math.min(1.0, score + boundaryBonus);
const lengthPenalty = Math.abs(query.length - n) /
Math.max(query.length, m);
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));
@ -319,7 +350,13 @@ class SearchEngine {
if (!text || typeof text !== "string") return [];
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
const tokens = words.filter((word) => word.length > 2);
const stopwordsSet = new Set(
this.config.stopwords.map((w) => w.toLowerCase()),
);
const tokens = words.filter(
(word) =>
word.length >= this.config.minWordLength && !stopwordsSet.has(word),
);
return Array.from(new Set(tokens));
}
@ -343,7 +380,6 @@ class SearchEngine {
}
if (!this.isLoaded || this.documents.length === 0) {
console.log("Search data not available");
return [];
}
@ -357,12 +393,23 @@ class SearchEngine {
const useFuzzySearch = rawQuery.length >= 3;
const candidateDocIds = new Set();
searchTerms.forEach((term) => {
if (this.tokenMap.has(term)) {
const docIds = this.tokenMap.get(term);
docIds.forEach((docId) => candidateDocIds.add(docId));
}
});
if (candidateDocIds.size === 0) {
return [];
}
const pageMatches = new Map();
const totalDocs = this.documents.length;
let lastCheckTime = Date.now();
const CHECK_INTERVAL = 16; // Check every ~16ms (one frame)
for (let docIdx = 0; docIdx < totalDocs; docIdx++) {
for (const docIdx of candidateDocIds) {
// Check for abort periodically
if (Date.now() - lastCheckTime > CHECK_INTERVAL) {
if (options.signal?.aborted) {
@ -384,33 +431,37 @@ class SearchEngine {
pageMatches.set(docIdx, match);
}
const lowerTitle = (
typeof doc.title === "string" ? doc.title : ""
).toLowerCase();
const lowerContent = (
typeof doc.content === "string" ? doc.content : ""
).toLowerCase();
const cached = this.lowercaseCache?.[docIdx];
const lowerTitle =
cached?.title ??
(typeof doc.title === "string" ? doc.title : "").toLowerCase();
const lowerContent =
cached?.content ??
(typeof doc.content === "string" ? doc.content : "").toLowerCase();
if (useFuzzySearch) {
const fuzzyTitleScore = this.fuzzyMatch(rawQuery, lowerTitle);
if (fuzzyTitleScore !== null) {
match.pageScore += fuzzyTitleScore * 100;
match.pageScore += fuzzyTitleScore * this.config.boostTitle;
}
const fuzzyContentScore = this.fuzzyMatch(rawQuery, lowerContent);
if (fuzzyContentScore !== null) {
match.pageScore += fuzzyContentScore * 30;
match.pageScore += fuzzyContentScore * this.config.boostContent;
}
}
searchTerms.forEach((term) => {
if (lowerTitle.includes(term)) {
match.pageScore += lowerTitle === term ? 20 : 10;
match.pageScore +=
lowerTitle === term
? this.config.boostTitle / 5
: this.config.boostTitle / 10;
}
if (lowerContent.includes(term)) {
match.pageScore += 2;
match.pageScore += this.config.boostContent / 15;
}
});
}
@ -629,9 +680,9 @@ class SearchEngine {
}
return new Promise((resolve, reject) => {
const messageId = `search_${Date.now()}_${
Math.random().toString(36).substring(2, 11)
}`;
const messageId = `search_${Date.now()}_${Math.random()
.toString(36)
.substring(2, 11)}`;
const timeout = setTimeout(() => {
cleanup();
reject(new Error("Web Worker search timeout"));
@ -664,14 +715,12 @@ class SearchEngine {
worker.addEventListener("message", handleMessage);
worker.addEventListener("error", handleError);
worker.postMessage(
{
messageId,
type: "search",
data: { query, limit },
documents: this.documents,
},
);
worker.postMessage({
messageId,
type: "search",
data: { query, limit },
documents: this.documents,
});
});
}
@ -681,7 +730,7 @@ class SearchEngine {
return text
.toLowerCase()
.replace(/\s+/g, " ")
.replace(/[.,!?;:'"…—–-]+$/g, "")
.replace(SearchEngine.STRIP_TRAILING_CHARS_RE, "")
.trim();
}
@ -829,13 +878,13 @@ class SearchEngine {
let pageScore = 0;
if (titleMatch !== -1) {
pageScore += 10;
pageScore += this.config.boostTitle / 10;
if (doc.title.toLowerCase() === lowerQuery) {
pageScore += 20;
pageScore += this.config.boostTitle / 5;
}
}
if (contentMatch !== -1) {
pageScore += 2;
pageScore += this.config.boostContent / 15;
}
// Find matching anchors
@ -872,6 +921,105 @@ class SearchEngine {
// Create Web Worker if supported - initialized lazily to use rootPath
let searchWorker = null;
// Keyboard navigation helper class
class SearchKeyboardNav {
constructor(container, selector) {
this.container = container;
this.selector = selector;
this.activeIndex = -1;
this.items = [];
this.navigationPending = false;
}
updateItems() {
this.items = Array.from(this.container.querySelectorAll(this.selector));
if (this.activeIndex >= this.items.length) {
this.activeIndex = -1;
}
}
clear() {
this.setActive(-1);
this.items = [];
}
setActive(index) {
// Remove active class from previous item
if (this.activeIndex >= 0 && this.activeIndex < this.items.length) {
this.items[this.activeIndex].classList.remove("search-result-active");
}
this.activeIndex = index;
// Add active class to new item
if (this.activeIndex >= 0 && this.activeIndex < this.items.length) {
this.items[this.activeIndex].classList.add("search-result-active");
this.items[this.activeIndex].scrollIntoView({
block: "nearest",
behavior: "smooth",
});
}
}
moveDown() {
if (this.items.length === 0) return;
const newIndex = Math.min(this.activeIndex + 1, this.items.length - 1);
this.setActive(newIndex);
}
moveUp() {
if (this.items.length === 0) return;
const newIndex = Math.max(this.activeIndex - 1, -1);
this.setActive(newIndex);
}
moveToFirst() {
if (this.items.length === 0) return;
this.setActive(0);
}
moveToLast() {
if (this.items.length === 0) return;
this.setActive(this.items.length - 1);
}
select() {
// Guard against double-navigation
if (this.navigationPending) return false;
if (this.activeIndex >= 0 && this.activeIndex < this.items.length) {
const link = this.items[this.activeIndex].querySelector("a");
if (link) {
this.navigationPending = true;
// Add search query to URL if it's a result link
const currentQuery =
this.container.closest(".search-container")?.querySelector("input")
?.value || document.getElementById("search-page-input")?.value;
if (currentQuery) {
const url = new URL(link.href, window.location.origin);
url.searchParams.set("highlight", currentQuery);
// Clear flag after navigation starts
setTimeout(() => {
this.navigationPending = false;
}, 100);
window.location.href = url.toString();
} else {
// Clear flag before click to allow navigation
setTimeout(() => {
this.navigationPending = false;
}, 100);
link.click();
}
return true;
}
}
return false;
}
}
function debounce(func, wait) {
let timeout = null;
return function (...args) {
@ -891,7 +1039,6 @@ function initializeSearchWorker() {
? `${rootPath}assets/search-worker.js`
: "/assets/search-worker.js";
searchWorker = new Worker(workerPath);
console.log("Web Worker initialized for background search");
return searchWorker;
} catch (error) {
console.warn("Web Worker creation failed, using main thread:", error);
@ -913,9 +1060,7 @@ document.addEventListener("DOMContentLoaded", function () {
// Initialize search engine immediately
window.searchNamespace.engine
.loadData()
.then(() => {
console.log("Search data loaded successfully");
})
.then(() => {})
.catch((error) => {
console.error("Failed to initialize search:", error);
});
@ -923,13 +1068,53 @@ document.addEventListener("DOMContentLoaded", function () {
// Search page specific functionality
const searchPageInput = document.getElementById("search-page-input");
if (searchPageInput) {
// Initialize keyboard navigation for search page
const searchPageResults = document.getElementById("search-page-results");
const searchPageKeyboardNav = new SearchKeyboardNav(
searchPageResults,
".search-result-item",
);
// Keyboard navigation for search page
searchPageInput.addEventListener("keydown", function (event) {
const hasResults =
searchPageResults &&
searchPageResults.querySelector(".search-result-item");
if (!hasResults) return;
if (event.key === "ArrowDown") {
event.preventDefault();
searchPageKeyboardNav.moveDown();
} else if (event.key === "ArrowUp") {
event.preventDefault();
searchPageKeyboardNav.moveUp();
} else if (event.key === "Home") {
event.preventDefault();
searchPageKeyboardNav.moveToFirst();
} else if (event.key === "End") {
event.preventDefault();
searchPageKeyboardNav.moveToLast();
} else if (
event.key === "Enter" &&
searchPageKeyboardNav.activeIndex >= 0
) {
event.preventDefault();
searchPageKeyboardNav.select();
} else if (event.key === "Escape") {
event.preventDefault();
searchPageKeyboardNav.clear();
searchPageInput.blur();
}
});
// Set up event listener with debouncing
searchPageInput.addEventListener(
"input",
debounce(function () {
const query = this.value.trim();
if (query.length >= 2) {
performSearch(query);
performSearch(query, searchPageKeyboardNav);
} else {
const resultsContainer = document.getElementById(
"search-page-results",
@ -938,6 +1123,7 @@ document.addEventListener("DOMContentLoaded", function () {
resultsContainer.innerHTML =
"<p>Please enter at least 2 characters to search</p>";
}
searchPageKeyboardNav.clear();
}
}, 200),
);
@ -947,7 +1133,7 @@ document.addEventListener("DOMContentLoaded", function () {
const query = params.get("q");
if (query) {
searchPageInput.value = query;
performSearch(query);
performSearch(query, searchPageKeyboardNav);
}
}
@ -956,6 +1142,11 @@ document.addEventListener("DOMContentLoaded", function () {
if (searchInput) {
const searchResults = document.getElementById("search-results");
const searchContainer = searchInput.closest(".search-container");
// Initialize keyboard navigation for desktop search
const desktopKeyboardNav = new SearchKeyboardNav(
searchResults,
".search-result-item",
);
searchInput.addEventListener(
"input",
@ -967,6 +1158,7 @@ document.addEventListener("DOMContentLoaded", function () {
searchResults.innerHTML = "";
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
desktopKeyboardNav.clear();
return;
}
@ -987,11 +1179,10 @@ document.addEventListener("DOMContentLoaded", function () {
searchResults.innerHTML = results
.map((result) => {
const { doc, matchingAnchors } = result;
const queryTerms = window.searchNamespace.engine.tokenize(
searchTerm,
);
const highlightedTitle = window.searchNamespace.engine
.highlightTerms(
const queryTerms =
window.searchNamespace.engine.tokenize(searchTerm);
const highlightedTitle =
window.searchNamespace.engine.highlightTerms(
doc.title,
queryTerms,
);
@ -1008,20 +1199,20 @@ document.addEventListener("DOMContentLoaded", function () {
if (matchingAnchors && matchingAnchors.length > 0) {
matchingAnchors.forEach((anchor) => {
// Skip anchors that duplicate the page title
const normalizedAnchor = window.searchNamespace.engine
.normalizeForComparison(
const normalizedAnchor =
window.searchNamespace.engine.normalizeForComparison(
anchor.text,
);
const normalizedTitle = window.searchNamespace.engine
.normalizeForComparison(
const normalizedTitle =
window.searchNamespace.engine.normalizeForComparison(
doc.title,
);
if (normalizedAnchor === normalizedTitle) {
return;
}
const highlightedAnchor = window.searchNamespace.engine
.highlightTerms(
const highlightedAnchor =
window.searchNamespace.engine.highlightTerms(
anchor.text,
queryTerms,
);
@ -1039,6 +1230,7 @@ document.addEventListener("DOMContentLoaded", function () {
.join("");
searchResults.style.display = "block";
if (searchContainer) searchContainer.classList.add("has-results");
desktopKeyboardNav.updateItems();
} else {
searchResults.innerHTML =
'<div class="search-result-item">No results found</div>';
@ -1048,7 +1240,7 @@ document.addEventListener("DOMContentLoaded", function () {
} catch (error) {
console.error("Search error:", error);
searchResults.innerHTML =
'<div class="search-result-item">Search unavailable</div>';
'<div class="search-result-item search-error" role="alert">Search unavailable. <a href="#" onclick="event.preventDefault(); window.searchNamespace.engine.loadData();">Retry</a></div>';
searchResults.style.display = "block";
if (searchContainer) searchContainer.classList.add("has-results");
}
@ -1063,6 +1255,35 @@ document.addEventListener("DOMContentLoaded", function () {
) {
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
desktopKeyboardNav.clear();
}
});
// Keyboard navigation for desktop search
searchInput.addEventListener("keydown", function (event) {
if (searchResults.style.display !== "block") return;
if (event.key === "ArrowDown") {
event.preventDefault();
desktopKeyboardNav.moveDown();
} else if (event.key === "ArrowUp") {
event.preventDefault();
desktopKeyboardNav.moveUp();
} else if (event.key === "Home") {
event.preventDefault();
desktopKeyboardNav.moveToFirst();
} else if (event.key === "End") {
event.preventDefault();
desktopKeyboardNav.moveToLast();
} else if (event.key === "Enter" && desktopKeyboardNav.activeIndex >= 0) {
event.preventDefault();
desktopKeyboardNav.select();
} else if (event.key === "Escape") {
event.preventDefault();
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
desktopKeyboardNav.clear();
searchInput.blur();
}
});
@ -1072,17 +1293,6 @@ document.addEventListener("DOMContentLoaded", function () {
event.preventDefault();
searchInput.focus();
}
// Close search results on Escape key
if (
event.key === "Escape" &&
(document.activeElement === searchInput ||
searchResults.style.display === "block")
) {
searchResults.style.display = "none";
if (searchContainer) searchContainer.classList.remove("has-results");
searchInput.blur();
}
});
setupDocumentEventHandlers(searchInput, searchResults, searchContainer);
@ -1094,8 +1304,8 @@ document.addEventListener("DOMContentLoaded", function () {
searchContainer,
) {
document.addEventListener("click", function (event) {
const isMobileSearchActive = mobileSearchPopup &&
mobileSearchPopup.classList.contains("active");
const isMobileSearchActive =
mobileSearchPopup && mobileSearchPopup.classList.contains("active");
const isDesktopResultsVisible = searchResults.style.display === "block";
if (
@ -1174,6 +1384,76 @@ document.addEventListener("DOMContentLoaded", function () {
const mobileSearchResults = document.getElementById("mobile-search-results");
const closeMobileSearchBtn = document.getElementById("close-mobile-search");
// Store cleanup function to prevent memory leaks
let mobileFocusTrapCleanup = null;
function setupMobileFocusTrap() {
if (!mobileSearchPopup || !mobileSearchPopup.classList.contains("active")) {
return;
}
const focusableElements = [
mobileSearchInput,
closeMobileSearchBtn,
...Array.from(mobileSearchResults.querySelectorAll("a[href]")),
].filter((el) => el !== null);
if (focusableElements.length === 0) return;
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
const handleKeyDown = (e) => {
if (e.key === "Tab") {
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
if (e.key === "Escape") {
closeMobileSearch();
}
// Arrow key navigation in results
if (["ArrowDown", "ArrowUp"].includes(e.key)) {
const links = Array.from(
mobileSearchResults.querySelectorAll("a[href]"),
);
if (links.length === 0) return;
const currentIndex = links.indexOf(document.activeElement);
if (e.key === "ArrowDown") {
e.preventDefault();
if (currentIndex === -1) {
links[0].focus();
} else {
const nextIndex = Math.min(currentIndex + 1, links.length - 1);
links[nextIndex].focus();
}
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (currentIndex > 0) {
links[currentIndex - 1].focus();
} else if (currentIndex === 0) {
mobileSearchInput.focus();
}
}
}
};
mobileSearchPopup.addEventListener("keydown", handleKeyDown);
// Return cleanup function
return () => {
mobileSearchPopup.removeEventListener("keydown", handleKeyDown);
};
}
function openMobileSearch() {
if (mobileSearchPopup) {
mobileSearchPopup.classList.add("active");
@ -1182,12 +1462,23 @@ document.addEventListener("DOMContentLoaded", function () {
if (mobileSearchInput) {
mobileSearchInput.focus();
}
// Clean up previous session's listeners before setting up new ones
if (mobileFocusTrapCleanup) {
mobileFocusTrapCleanup();
mobileFocusTrapCleanup = null;
}
mobileFocusTrapCleanup = setupMobileFocusTrap();
}, 100);
}
}
function closeMobileSearch() {
if (mobileSearchPopup) {
// Clean up event listeners before closing
if (mobileFocusTrapCleanup) {
mobileFocusTrapCleanup();
mobileFocusTrapCleanup = null;
}
mobileSearchPopup.classList.remove("active");
if (mobileSearchInput) {
mobileSearchInput.value = "";
@ -1235,11 +1526,10 @@ document.addEventListener("DOMContentLoaded", function () {
mobileSearchResults.innerHTML = results
.map((result) => {
const { doc, matchingAnchors } = result;
const queryTerms = window.searchNamespace.engine.tokenize(
searchTerm,
);
const highlightedTitle = window.searchNamespace.engine
.highlightTerms(
const queryTerms =
window.searchNamespace.engine.tokenize(searchTerm);
const highlightedTitle =
window.searchNamespace.engine.highlightTerms(
doc.title,
queryTerms,
);
@ -1258,25 +1548,25 @@ document.addEventListener("DOMContentLoaded", function () {
if (matchingAnchors && matchingAnchors.length > 0) {
matchingAnchors.forEach((anchor) => {
// Skip anchors that duplicate the page title
const normalizedAnchor = window.searchNamespace.engine
.normalizeForComparison(
const normalizedAnchor =
window.searchNamespace.engine.normalizeForComparison(
anchor.text,
);
const normalizedTitle = window.searchNamespace.engine
.normalizeForComparison(
const normalizedTitle =
window.searchNamespace.engine.normalizeForComparison(
doc.title,
);
if (normalizedAnchor === normalizedTitle) {
return;
}
const highlightedAnchor = window.searchNamespace.engine
.highlightTerms(
const highlightedAnchor =
window.searchNamespace.engine.highlightTerms(
anchor.text,
queryTerms,
);
const sectionPreview = window.searchNamespace.engine
.generateSectionPreview(
const sectionPreview =
window.searchNamespace.engine.generateSectionPreview(
doc,
anchor,
searchTerm,
@ -1298,6 +1588,12 @@ document.addEventListener("DOMContentLoaded", function () {
})
.join("");
mobileSearchResults.style.display = "block";
// Clean up previous listeners before setting up new ones
if (mobileFocusTrapCleanup) {
mobileFocusTrapCleanup();
mobileFocusTrapCleanup = null;
}
mobileFocusTrapCleanup = setupMobileFocusTrap();
} else {
mobileSearchResults.innerHTML =
'<div class="search-result-item">No results found</div>';
@ -1308,7 +1604,7 @@ document.addEventListener("DOMContentLoaded", function () {
// Verify once more
if (mobileSearchInput.value.trim() !== searchTerm) return;
mobileSearchResults.innerHTML =
'<div class="search-result-item">Search unavailable</div>';
'<div class="search-result-item search-error" role="alert">Search unavailable. <a href="#" onclick="event.preventDefault(); window.searchNamespace.engine.loadData();">Retry</a></div>';
mobileSearchResults.style.display = "block";
}
}, 300);
@ -1330,13 +1626,14 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
async function performSearch(query) {
async function performSearch(query, keyboardNav = null) {
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>";
if (keyboardNav) keyboardNav.clear();
return;
}
@ -1348,6 +1645,7 @@ async function performSearch(query) {
// Show loading state
resultsContainer.innerHTML = "<p>Searching...</p>";
if (keyboardNav) keyboardNav.clear();
try {
const results = await window.searchNamespace.engine.search(query, 50, {
@ -1390,21 +1688,21 @@ async function performSearch(query) {
if (matchingAnchors && matchingAnchors.length > 0) {
matchingAnchors.forEach((anchor) => {
// Skip anchors that have the same text as the page title to avoid duplication
const normalizedAnchor = window.searchNamespace.engine
.normalizeForComparison(anchor.text);
const normalizedTitle = window.searchNamespace.engine
.normalizeForComparison(doc.title);
const normalizedAnchor =
window.searchNamespace.engine.normalizeForComparison(anchor.text);
const normalizedTitle =
window.searchNamespace.engine.normalizeForComparison(doc.title);
if (normalizedAnchor === normalizedTitle) {
return;
}
const highlightedAnchor = window.searchNamespace.engine
.highlightTerms(
const highlightedAnchor =
window.searchNamespace.engine.highlightTerms(
anchor.text,
queryTerms,
);
const sectionPreview = window.searchNamespace.engine
.generateSectionPreview(
const sectionPreview =
window.searchNamespace.engine.generateSectionPreview(
doc,
anchor,
query,
@ -1421,8 +1719,10 @@ async function performSearch(query) {
}
html += "</ul>";
resultsContainer.innerHTML = html;
if (keyboardNav) keyboardNav.updateItems();
} else {
resultsContainer.innerHTML = "<p>No results found</p>";
if (keyboardNav) keyboardNav.clear();
}
// Update URL with query
@ -1434,6 +1734,14 @@ async function performSearch(query) {
return;
}
console.error("Search error:", error);
resultsContainer.innerHTML = "<p>Search temporarily unavailable</p>";
resultsContainer.innerHTML = `
<div class="search-error" role="alert">
<p>Search is temporarily unavailable. Please try again.</p>
<button type="button" onclick="window.searchNamespace.engine.loadData().then(() => { this.closest('.search-error').innerHTML = '<p>Search reloaded. Please try your search again.</p>'; })">
Retry
</button>
</div>
`;
if (keyboardNav) keyboardNav.clear();
}
}

View file

@ -113,6 +113,9 @@
0 10px 15px -3px var(--shadow-color), 0 4px 6px -4px var(--shadow-color);
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition: 250ms cubic-bezier(0.4, 0, 0.2, 1);
/* Fonts */
--font-mono: "JetBrains Mono", monospace;
}
@media (prefers-color-scheme: dark) {
@ -631,7 +634,7 @@ a:hover {
/* Code Styling */
code {
font-family: "JetBrains Mono", monospace;
font-family: var(--font-mono);
background-color: var(--code-bg);
border-radius: var(--radius-sm);
padding: 0.2em 0.4em;
@ -806,6 +809,9 @@ img {
padding: var(--space-4);
border-bottom: 1px solid var(--border-color);
transition: border-color var(--transition);
min-height: 44px;
display: flex;
align-items: center;
}
.search-result-item:last-child {
@ -816,12 +822,18 @@ img {
color: var(--text-color);
text-decoration: none;
display: block;
flex-grow: 1;
}
.search-result-item:hover {
background-color: var(--sidebar-hover);
}
.search-result-item.search-result-active {
background-color: var(--sidebar-active);
border-left: 3px solid var(--link-color);
}
.search-result-title {
font-weight: 600;
margin-bottom: var(--space-2);
@ -879,7 +891,8 @@ img {
#mobile-search-input {
flex-grow: 1;
padding: 10px 15px;
padding: var(--space-3) var(--space-4);
min-height: 48px;
font-size: 1.1em;
border: 1px solid var(--border-color);
border-radius: 4px;
@ -899,9 +912,14 @@ img {
font-size: 2rem;
color: var(--text-muted);
cursor: pointer;
padding: 0 0.5rem;
padding: 0;
margin-left: 0.5rem;
line-height: 1;
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.close-mobile-search:hover {
@ -915,7 +933,10 @@ img {
/* Reuse desktop search result styling */
.mobile-search-results .search-result-item {
padding: 10px;
padding: var(--space-3) var(--space-4);
min-height: 44px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
@ -926,13 +947,24 @@ img {
.mobile-search-results .search-result-item a {
text-decoration: none;
color: inherit;
display: block;
display: flex;
align-items: center;
padding: var(--space-2) 0;
min-height: 44px;
flex-grow: 1;
}
.mobile-search-results .search-result-item:hover {
background-color: var(--sidebar-hover);
}
/* Touch feedback for mobile devices */
@media (hover: none) and (pointer: coarse) {
.mobile-search-results .search-result-item:active {
background-color: var(--sidebar-active);
}
}
.mobile-search-results .search-result-title {
font-weight: 600;
color: var(--heading-color);
@ -981,6 +1013,50 @@ img {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.search-keyboard-hints {
display: flex;
gap: var(--space-4);
margin-top: var(--space-3);
padding: var(--space-2) var(--space-3);
background-color: var(--sidebar-bg);
border-radius: var(--radius-sm);
font-size: 0.875rem;
color: var(--text-muted);
flex-wrap: wrap;
}
.search-keyboard-hints .hint-item {
display: flex;
align-items: center;
gap: var(--space-1);
}
.search-keyboard-hints kbd {
display: inline-block;
padding: 0.125rem 0.375rem;
font-size: 0.75rem;
font-family: ui-monospace, monospace;
background-color: var(--code-bg);
border: 1px solid var(--border-color);
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
color: var(--text-color);
}
@media (prefers-color-scheme: dark) {
.search-keyboard-hints kbd {
background-color: var(--sidebar-bg);
border-color: var(--border-color);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
}
@media (max-width: 800px) {
.search-keyboard-hints {
display: none;
}
}
.search-page-results {
margin-top: var(--space-6);
}
@ -1658,7 +1734,7 @@ h6:hover .copy-link {
.toc-list details summary {
cursor: pointer;
padding: var(--space-2) var(--space-3);
font-weight: 500;
font-weight: 400;
font-size: 0.875rem;
color: var(--text-color);
display: flex;
@ -1683,11 +1759,10 @@ h6:hover .copy-link {
display: none;
}
.toc-list details summary::after {
.toc-list details summary::before {
content: "▶";
font-size: 0.65rem;
margin-left: auto;
margin-right: var(--space-1);
margin-right: var(--space-2);
flex-shrink: 0;
color: var(--text-muted);
transition:
@ -1695,7 +1770,7 @@ h6:hover .copy-link {
color var(--transition-fast);
}
.toc-list details[open] summary::after {
.toc-list details[open] summary::before {
transform: rotate(90deg);
color: var(--link-color);
}
@ -1713,7 +1788,7 @@ h6:hover .copy-link {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: var(--space-2);
margin-left: auto;
flex-shrink: 0;
}
@ -1800,6 +1875,151 @@ h6:hover .copy-link {
margin-top: var(--space-3);
}
/* Lib entry specific styling */
.lib-entry {
scroll-margin-top: 80px;
padding: var(--space-6);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
margin: var(--space-6) 0;
background-color: var(--sidebar-bg);
transition:
background-color var(--transition),
border-color var(--transition);
box-shadow: var(--shadow-sm);
}
.lib-entry:target,
.lib-entry.highlight {
border-color: var(--link-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
animation: highlight-pulse 1.5s ease;
}
.content .lib-entry-name {
margin-top: 0;
margin-bottom: var(--space-3);
display: flex;
align-items: center;
}
.lib-entry-anchor {
color: var(--heading-color);
text-decoration: none;
}
.lib-entry-anchor:hover {
color: var(--link-color);
text-decoration: none;
}
.lib-entry-declared {
color: var(--text-muted);
font-style: italic;
margin-top: var(--space-3);
}
.lib-entry-type {
margin-bottom: var(--space-3);
}
.lib-entry-description {
margin-bottom: var(--space-4);
}
.lib-entry-arguments h3,
.lib-entry-examples h3,
.lib-entry-notes h3,
.lib-entry-warnings h3 {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-muted);
margin: var(--space-4) 0 var(--space-2) 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.lib-entry-arguments dl {
margin: 0;
}
.lib-entry-arguments dt {
font-weight: 600;
}
.lib-entry-arguments dd {
margin-left: var(--space-6);
margin-bottom: var(--space-2);
color: var(--text-muted);
}
.lib-entry-examples pre {
margin: var(--space-2) 0;
padding: var(--space-3);
background-color: var(--code-bg);
border-radius: var(--radius-sm);
overflow-x: auto;
}
.lib-entry-examples code {
font-family: var(--font-mono);
font-size: 0.875em;
}
.lib-entry-deprecated {
margin-top: var(--space-3);
padding: var(--space-3);
background-color: color-mix(
in srgb,
var(--admonition-warning-color) 10%,
transparent
);
border-left: 3px solid var(--admonition-warning-color);
border-radius: var(--radius-sm);
}
.lib-entry-deprecated p {
margin: 0;
color: var(--admonition-warning-color);
}
.lib-entry-notes ul,
.lib-entry-warnings ul {
margin: 0;
padding-left: var(--space-6);
}
.lib-entry-notes li,
.lib-entry-warnings li {
margin-bottom: var(--space-1);
}
.lib-entry-warnings {
margin-top: var(--space-3);
padding: var(--space-3);
background-color: color-mix(
in srgb,
var(--admonition-danger-color) 10%,
transparent
);
border-left: 3px solid var(--admonition-danger-color);
border-radius: var(--radius-sm);
}
.lib-entry-warnings h3 {
color: var(--admonition-danger-color);
}
/* Lib container */
.lib-container {
position: relative;
overflow: visible;
min-height: 0;
height: auto;
margin-bottom: 0;
padding-bottom: 0;
}
/* Filter styling */
.search-form {
margin: var(--space-4) 0;
@ -1808,6 +2028,7 @@ h6:hover .copy-link {
box-sizing: border-box;
}
#lib-filter,
#options-filter {
width: 100%;
padding: var(--space-3);
@ -1820,6 +2041,7 @@ h6:hover .copy-link {
box-sizing: border-box;
}
#lib-filter:focus,
#options-filter:focus {
outline: none;
border-color: var(--link-color);
@ -2005,7 +2227,8 @@ h6:hover .copy-link {
margin-bottom: var(--space-2);
}
.sidebar-section summary {
/* Only target direct child summary (section headers like "Documents", "Contents") */
.sidebar-section > summary {
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
@ -2023,24 +2246,24 @@ h6:hover .copy-link {
background-color var(--transition-fast);
}
.sidebar-section summary:hover {
.sidebar-section > summary:hover {
background-color: var(--sidebar-hover);
color: var(--text-color);
}
.sidebar-section summary::before {
.sidebar-section > summary::before {
content: "▶";
font-size: 0.625rem;
transition: transform var(--transition-fast);
flex-shrink: 0;
}
.sidebar-section[open] summary::before {
.sidebar-section[open] > summary::before {
transform: rotate(90deg);
}
/* Hide default marker */
.sidebar-section summary::-webkit-details-marker {
.sidebar-section > summary::-webkit-details-marker {
display: none;
}
@ -2141,3 +2364,176 @@ h6:hover .copy-link {
display: none !important;
}
}
/* Keyboard navigation active state */
.search-result-item.search-result-active {
background-color: var(--sidebar-active);
border-left: 3px solid var(--link-color);
}
/* Search error styling */
.search-error {
padding: var(--space-4);
border: 1px solid var(--admonition-danger-color);
border-radius: var(--radius-md);
background-color: var(--sidebar-bg);
color: var(--text-color);
}
.search-error p {
margin: 0 0 var(--space-3) 0;
}
.search-error button,
.search-error a {
display: inline-block;
padding: var(--space-2) var(--space-4);
background-color: var(--link-color);
color: white;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
text-decoration: none;
font-size: 0.875rem;
transition: background-color var(--transition-fast);
}
.search-error button:hover,
.search-error a:hover {
background-color: var(--link-hover-color);
}
.search-result-item.search-result-active a {
color: var(--link-color);
}
/* Search result highlighting on target pages */
.search-highlight {
background-color: rgba(255, 235, 59, 0.4);
color: inherit;
font-weight: inherit;
padding: 2px 0;
border-radius: 2px;
animation: highlight-fade 3s ease-out forwards;
}
.search-highlight-active {
background-color: rgba(255, 193, 7, 0.6);
animation: highlight-pulse-active 1s ease-in-out infinite alternate;
}
@keyframes highlight-fade {
0% {
background-color: rgba(255, 235, 59, 0.6);
}
100% {
background-color: rgba(255, 235, 59, 0.2);
}
}
@keyframes highlight-pulse-active {
0% {
background-color: rgba(255, 193, 7, 0.5);
}
100% {
background-color: rgba(255, 193, 7, 0.7);
}
}
@media (prefers-color-scheme: dark) {
.search-highlight {
background-color: rgba(255, 193, 7, 0.3);
animation: highlight-fade-dark 3s ease-out forwards;
}
.search-highlight-active {
background-color: rgba(255, 152, 0, 0.5);
animation: highlight-pulse-active-dark 1s ease-in-out infinite alternate;
}
@keyframes highlight-fade-dark {
0% {
background-color: rgba(255, 193, 7, 0.4);
}
100% {
background-color: rgba(255, 193, 7, 0.15);
}
}
@keyframes highlight-pulse-active-dark {
0% {
background-color: rgba(255, 152, 0, 0.4);
}
100% {
background-color: rgba(255, 152, 0, 0.6);
}
}
}
/* Sidebar directory groups */
.sidebar-dir-group {
list-style: none;
margin-bottom: var(--space-1);
> details {
> summary {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
cursor: pointer;
user-select: none;
font-size: 0.8125rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
list-style: none;
transition:
background-color var(--transition-fast),
color var(--transition-fast);
&::-webkit-details-marker {
display: none;
}
&::before {
content: "▶";
font-size: 0.625rem;
transition: transform var(--transition-fast);
flex-shrink: 0;
}
&:hover {
background-color: var(--sidebar-hover);
color: var(--text-color);
}
}
&[open] > summary::before {
transform: rotate(90deg);
}
> ul {
margin: var(--space-1) 0 var(--space-1) var(--space-4);
padding-left: var(--space-2);
border-left: 2px solid var(--border-color);
}
}
.sidebar-dir-count {
font-size: 0.7rem;
font-weight: 500;
background-color: var(--sidebar-active);
color: var(--text-muted);
border-radius: 9999px;
padding: 0 var(--space-2);
line-height: 1.6;
margin-left: auto;
}
.sidebar-dir-label {
flex: 1;
}
}

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -5,11 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Introduction</title>
<script>
// Apply sidebar state immediately to prevent flash
(function () {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
try {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
} catch (e) {
// localStorage unavailable
}
})();
</script>
@ -111,7 +116,7 @@
</nav>
<main class="content"><html><head></head><body><h1 id="nvf-manual">Introduction</h1>
<p>Generated for nvf v0.9</p>
<p>Generated for nvf unstable</p>
<h2 id="ch-preface">Preface</h2>
<h3 id="sec-what-is-it">What is nvf</h3>
<p><strong>nvf</strong> is a highly modular, configurable, extensible and <em>easy to use</em> Neovim
@ -264,7 +269,7 @@ configure <strong>nvf</strong>.</p>
<p class="admonition-title">Note</p>
<p><strong>nvf</strong> exposes a lot of options, most of which are not referenced in the
installation sections of the manual. You may find all available options in the
<a href="https://notashelf.github.io/nvf/options">appendix</a></p>
<a href="https://notashelf.github.io/nvf/options.html">appendix</a></p>
</div>
<h2 id="sec-nixos-flakeless">Without Flakes</h2>
<p>As of v0.8, it is possible to install <strong>nvf</strong> on a system if you are not using
@ -334,7 +339,7 @@ configure <strong>nvf</strong>.</p>
<p class="admonition-title">Note</p>
<p><strong>nvf</strong> exposes a lot of options, most of which are not referenced in the
installation sections of the manual. You may find all available options in the
<a href="https://notashelf.github.io/nvf/options">appendix</a></p>
<a href="https://notashelf.github.io/nvf/options.html">appendix</a></p>
</div>
<h2 id="sec-hm-flakeless">Without Flakes</h2>
<p>As of v0.8, it is possible to install <strong>nvf</strong> on a system if you are not using

14577
options.html

File diff suppressed because it is too large Load diff

View file

@ -5,11 +5,16 @@
<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");
try {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
} catch (e) {
// localStorage unavailable
}
})();
</script>

View file

@ -5,11 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Release Notes</title>
<script>
// Apply sidebar state immediately to prevent flash
(function () {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
try {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
} catch (e) {
// localStorage unavailable
}
})();
</script>
@ -138,7 +143,7 @@ setting. Use <code>workspaces</code> instead:</p>
<p>Some other settings and commands are now deprecated but are still supported.</p>
<ul>
<li>The <code>setupOpts.mappings</code> options were also removed. Use the built-in Neovim
settings (nvf's <a class="option-reference" href="options.html#option-vim-keymaps"><code class="nixos-option">vim.keymaps</code></a>)</li>
settings (nvf's <a class="option-reference" href="options.html#option-vim.keymaps"><code class="nixos-option">vim.keymaps</code></a>)</li>
</ul>
</li>
<li>
@ -256,9 +261,9 @@ deprecated and thus was pulled from nixpkgs.
<p>Renamed <code>languages.ts</code> to <code>languages.typescript</code>.</p>
</li>
<li>
<p>Added <a class="option-reference" href="options.html#option-vim-languages-go-treesitter-gotmpl-injection"><code class="nixos-option">vim.languages.go.treesitter.gotmpl.injection</code></a> and Renamed
<p>Added <a class="option-reference" href="options.html#option-vim.languages.go.treesitter.gotmpl.injection"><code class="nixos-option">vim.languages.go.treesitter.gotmpl.injection</code></a> and Renamed
<code>languages.go.treesitter.gotmplPackage</code> to
<a class="option-reference" href="options.html#option-vim-languages-go-treesitter-gotmpl-package"><code class="nixos-option">vim.languages.go.treesitter.gotmpl.package</code></a></p>
<a class="option-reference" href="options.html#option-vim.languages.go.treesitter.gotmpl.package"><code class="nixos-option">vim.languages.go.treesitter.gotmpl.package</code></a></p>
</li>
</ul>
<h2 id="sec-release-0-9-changelog">Changelog</h2>
@ -310,7 +315,7 @@ values in <code>vim.treesitter.grammars</code>.</li>
</ul>
<p><a href="https://github.com/jfeo">jfeo</a>:</p>
<ul>
<li>Added <a href="https://github.com/uga-rosa/ccc.nvim">ccc.nvim</a> option <a class="option-reference" href="options.html#option-vim-utility-ccc-setupOpts"><code class="nixos-option">vim.utility.ccc.setupOpts</code></a> with the existing
<li>Added <a href="https://github.com/uga-rosa/ccc.nvim">ccc.nvim</a> option <a class="option-reference" href="options.html#option-vim.utility.ccc.setupOpts"><code class="nixos-option">vim.utility.ccc.setupOpts</code></a> with the existing
hard-coded options as default values.</li>
</ul>
<p><a href="https://github.com/Ring-A-Ding-Ding-Baby">Ring-A-Ding-Ding-Baby</a>:</p>
@ -434,7 +439,7 @@ actually correspond to any keybinds.</p>
support to <code>languages.python</code></p>
</li>
<li>
<p>Added TOML support via <a class="option-reference" href="options.html#option-vim-languages-toml-enable"><code class="nixos-option">vim.languages.toml.enable</code></a> and the
<p>Added TOML support via <a class="option-reference" href="options.html#option-vim.languages.toml.enable"><code class="nixos-option">vim.languages.toml.enable</code></a> and the
<a href="https://tombi-toml.github.io/tombi/">Tombi</a> language server, linter, and
formatter.</p>
</li>
@ -460,14 +465,14 @@ formatter.</p>
<p><a href="https://github.com/snoweuph">Snoweuph</a></p>
<ul>
<li>
<p>Added <a class="option-reference" href="options.html#option-vim-treesitter-queries"><code class="nixos-option">vim.treesitter.queries</code></a> to support adding custom queries.</p>
<p>Added <a class="option-reference" href="options.html#option-vim.treesitter.queries"><code class="nixos-option">vim.treesitter.queries</code></a> to support adding custom queries.</p>
</li>
<li>
<p>Added injections for <code>vim.treesitter.queries.*.content</code> as <code>query</code> and
<code>mkLualine</code>, <code>entryAnywhere</code>, <code>entryBefore</code>, <code>entryAfter</code> as <code>lua</code> in nix.</p>
</li>
<li>
<p>Added <a class="option-reference" href="options.html#option-vim-languages-tera-treesitter-injection"><code class="nixos-option">vim.languages.tera.treesitter.injection</code></a> to configure, what
<p>Added <a class="option-reference" href="options.html#option-vim.languages.tera.treesitter.injection"><code class="nixos-option">vim.languages.tera.treesitter.injection</code></a> to configure, what
language the content is.</p>
</li>
<li>
@ -476,7 +481,7 @@ more flexibility in nvf and reuse of LSPs across languages. Dropped
<code>deprecatedSingleOrListOf</code> in favor of <code>listOf</code> for the affected LSP options.</p>
</li>
<li>
<p>Added <a class="option-reference" href="options.html#option-vim-lsp-presets-angular-language-server-enable"><code class="nixos-option">vim.lsp.presets.angular-language-server.enable</code></a> for Angular
<p>Added <a class="option-reference" href="options.html#option-vim.lsp.presets.angular-language-server.enable"><code class="nixos-option">vim.lsp.presets.angular-language-server.enable</code></a> for Angular
Template support.</p>
</li>
<li>
@ -488,7 +493,7 @@ Template support.</p>
out.</p>
</li>
<li>
<p>Fix <a class="option-reference" href="options.html#option-vim-utility-nvim-biscuits-enable"><code class="nixos-option">vim.utility.nvim-biscuits.enable</code></a> by upgrading, to fix
<p>Fix <a class="option-reference" href="options.html#option-vim.utility.nvim-biscuits.enable"><code class="nixos-option">vim.utility.nvim-biscuits.enable</code></a> by upgrading, to fix
tree-sitter incompatibilities.</p>
</li>
<li>
@ -605,11 +610,11 @@ previewing yet.</p>
<p>Extend <code>languages.asm</code> to support more filetypes out of the box.</p>
</li>
<li>
<p>Added <a class="option-reference" href="options.html#option-vim-languages-java-extensions-maven-nvim-enable"><code class="nixos-option">vim.languages.java.extensions.maven-nvim.enable</code></a> for Maven
<p>Added <a class="option-reference" href="options.html#option-vim.languages.java.extensions.maven-nvim.enable"><code class="nixos-option">vim.languages.java.extensions.maven-nvim.enable</code></a> for Maven
support;</p>
</li>
<li>
<p>Added <a class="option-reference" href="options.html#option-vim-languages-java-extensions-gradle-nvim-enable"><code class="nixos-option">vim.languages.java.extensions.gradle-nvim.enable</code></a> for Gradle
<p>Added <a class="option-reference" href="options.html#option-vim.languages.java.extensions.gradle-nvim.enable"><code class="nixos-option">vim.languages.java.extensions.gradle-nvim.enable</code></a> for Gradle
support;</p>
</li>
<li>
@ -725,7 +730,7 @@ upstream.</p>
<li>
<p><code>vim.useSystemClipboard</code> has been deprecated as a part of removing most
top-level convenience options, and should instead be configured in the new
module interface. You may set <a class="option-reference" href="options.html#option-vim-clipboard-registers"><code class="nixos-option">vim.clipboard.registers</code></a> appropriately
module interface. You may set <a class="option-reference" href="options.html#option-vim.clipboard.registers"><code class="nixos-option">vim.clipboard.registers</code></a> appropriately
to configure Neovim to use the system clipboard.</p>
</li>
<li>
@ -754,18 +759,18 @@ can remove them now.</p>
<code>languages.markdown.extensions.render-markdown-nvim</code>.</p>
</li>
<li>
<p>Implement <a class="option-reference" href="options.html#option-vim-git-gitsigns-setupOpts"><code class="nixos-option">vim.git.gitsigns.setupOpts</code></a> for user-specified setup table
<p>Implement <a class="option-reference" href="options.html#option-vim.git.gitsigns.setupOpts"><code class="nixos-option">vim.git.gitsigns.setupOpts</code></a> for user-specified setup table
in gitsigns configuration.</p>
</li>
<li>
<p><a class="option-reference" href="options.html#option-vim-options-mouse"><code class="nixos-option">vim.options.mouse</code></a> no longer compares values to an enum of available
<p><a class="option-reference" href="options.html#option-vim.options.mouse"><code class="nixos-option">vim.options.mouse</code></a> no longer compares values to an enum of available
mouse modes. This means you can provide any string without the module system
warning you that it is invalid. Do keep in mind that this value is no longer
checked, so you will be responsible for ensuring its validity.</p>
</li>
<li>
<p>Deprecate <code>vim.enableEditorconfig</code> in favor of
<a class="option-reference" href="options.html#option-vim-globals-editorconfig"><code class="nixos-option">vim.globals.editorconfig</code></a>.</p>
<a class="option-reference" href="options.html#option-vim.globals.editorconfig"><code class="nixos-option">vim.globals.editorconfig</code></a>.</p>
</li>
<li>
<p>Deprecate rnix-lsp as it has been abandoned and archived upstream.</p>
@ -777,7 +782,7 @@ your Editorconfig configuration, or use an autocommand to set indentation
values for buffers with the Nix filetype.</p>
</li>
<li>
<p>Add <a class="option-reference" href="options.html#option-vim-lsp-lightbulb-autocmd-enable"><code class="nixos-option">vim.lsp.lightbulb.autocmd.enable</code></a> for manually managing the
<p>Add <a class="option-reference" href="options.html#option-vim.lsp.lightbulb.autocmd.enable"><code class="nixos-option">vim.lsp.lightbulb.autocmd.enable</code></a> for manually managing the
previously managed lightbulb autocommand.</p>
<ul>
<li>A warning will occur if {option} vim-lsp-lightbulb-autocmd-enable) and
@ -796,7 +801,7 @@ backend while shada is disabled in Neovim options.</p>
<p>Add <a href="https://github.com/mikavilpas/yazi.nvim">yazi.nvim</a> as a companion plugin for Yazi, the terminal file manager.</p>
</li>
<li>
<p>Add <a class="option-reference" href="options.html#option-vim-autocmds"><code class="nixos-option">vim.autocmds</code></a> and <a class="option-reference" href="options.html#option-vim-augroups"><code class="nixos-option">vim-augroups</code></a> to allow declaring
<p>Add <a class="option-reference" href="options.html#option-vim.autocmds"><code class="nixos-option">vim.autocmds</code></a> and <a class="option-reference" href="options.html#option-vim.augroups"><code class="nixos-option">vim.augroups</code></a> to allow declaring
autocommands via Nix.</p>
</li>
<li>
@ -828,7 +833,7 @@ integration for blink-cmp and nvim-cmp</p>
<li>
<p>Add <code>vim.diagnostics</code> to interact with Neovim's diagnostics module. Available
options for <code>vim.diagnostic.config()</code> can now be customized through the
<a class="option-reference" href="options.html#option-vim-diagnostics-config"><code class="nixos-option">vim.diagnostics.config</code></a> in nvf.</p>
<a class="option-reference" href="options.html#option-vim.diagnostics.config"><code class="nixos-option">vim.diagnostics.config</code></a> in nvf.</p>
</li>
<li>
<p>Add <code>vim.clipboard</code> module for easily managing Neovim clipboard providers and
@ -939,7 +944,7 @@ issue with setting the workspace directory.</li>
<li>Add <a href="https://github.com/ibhagwan/fzf-lua">fzf-lua</a> in <code>vim.fzf-lua</code></li>
<li>Add <a href="https://github.com/HiPhish/rainbow-delimiters.nvim">rainbow-delimiters</a>
in <code>vim.visuals.rainbow-delimiters</code></li>
<li>Add options to define highlights under <a class="option-reference" href="options.html#option-vim-highlight"><code class="nixos-option">vim.highlight</code></a></li>
<li>Add options to define highlights under <a class="option-reference" href="options.html#option-vim.highlight"><code class="nixos-option">vim.highlight</code></a></li>
</ul>
<p><a href="https://github.com/kaktu5">kaktu5</a>:</p>
<ul>
@ -1360,7 +1365,7 @@ it to something other than <code>mapleader</code> to avoid conflicts.</p>
options that were under <code>vim</code> as convenient shorthands for <code>vim.o.*</code> options.</p>
<div class="admonition warning">
<p class="admonition-title">Warning</p>
<p>As v0.7 features the addition of <a class="option-reference" href="options.html#option-vim-options"><code class="nixos-option">vim.options</code></a>, those options are now
<p>As v0.7 features the addition of <a class="option-reference" href="options.html#option-vim.options"><code class="nixos-option">vim.options</code></a>, those options are now
considered as deprecated. You should migrate to the appropriate options in the
<code>vim.options</code> submodule.</p>
</div>
@ -1369,7 +1374,7 @@ considered as deprecated. You should migrate to the appropriate options in the
<li>
<p><code>colourTerm</code>, <code>mouseSupport</code>, <code>cmdHeight</code>, <code>updateTime</code>, <code>mapTime</code>,
<code>cursorlineOpt</code>, <code>splitBelow</code>, <code>splitRight</code>, <code>autoIndent</code> and <code>wordWrap</code> have
been mapped to their <a class="option-reference" href="options.html#option-vim-options"><code class="nixos-option">vim.options</code></a> equivalents. Please see the module
been mapped to their <a class="option-reference" href="options.html#option-vim.options"><code class="nixos-option">vim.options</code></a> equivalents. Please see the module
definition for the updated options.</p>
</li>
<li>
@ -1388,7 +1393,7 @@ will enable the <code>typst-lsp</code> language server, and the <code>typstfmt</
<ul>
<li>
<p>Modified type for
<a class="option-reference" href="options.html#option-vim-visuals-fidget-nvim-setupOpts-progress-display-overrides"><code class="nixos-option">vim.visuals.fidget-nvim.setupOpts.progress.display.overrides</code></a> from
<a class="option-reference" href="options.html#option-vim.visuals.fidget-nvim.setupOpts.progress.display.overrides"><code class="nixos-option">vim.visuals.fidget-nvim.setupOpts.progress.display.overrides</code></a> from
<code>anything</code> to a <code>submodule</code> for better type checking.</p>
</li>
<li>
@ -1400,7 +1405,7 @@ group for <code>Normal</code>, <code>NormalFloat</code>, <code>LineNr</code>, <c
<code>NvimTreeNormal</code> to <code>none</code>.</p>
</li>
<li>
<p>Fix <a class="option-reference" href="options.html#option-vim-ui-smartcolumn-setupOpts-custom_colorcolumn"><code class="nixos-option">vim.ui.smartcolumn.setupOpts.custom_colorcolumn</code></a> using the wrong
<p>Fix <a class="option-reference" href="options.html#option-vim.ui.smartcolumn.setupOpts.custom_colorcolumn"><code class="nixos-option">vim.ui.smartcolumn.setupOpts.custom_colorcolumn</code></a> using the wrong
type <code>int</code> instead of the expected type <code>string</code>.</p>
</li>
</ul>
@ -1495,19 +1500,19 @@ aren't defined in nvf. Move the alternate nvim-surround keybinds to use
<li>
<p>Remove <code>autopairs.type</code>, and rename <code>autopairs.enable</code> to
<code>autopairs.nvim-autopairs.enable</code>. The new
<a class="option-reference" href="options.html#option-vim-autopairs-nvim-autopairs-enable"><code class="nixos-option">vim.autopairs.nvim-autopairs.enable</code></a> supports <code>setupOpts</code> format by
<a class="option-reference" href="options.html#option-vim.autopairs.nvim-autopairs.enable"><code class="nixos-option">vim.autopairs.nvim-autopairs.enable</code></a> supports <code>setupOpts</code> format by
default.</p>
</li>
<li>
<p>Refactor of <code>nvim-cmp</code> and completion related modules</p>
<ul>
<li>Remove <code>autocomplete.type</code> in favor of per-plugin enable options such as
<a class="option-reference" href="options.html#option-vim-autocomplete-nvim-cmp-enable"><code class="nixos-option">vim.autocomplete.nvim-cmp.enable</code></a>.</li>
<a class="option-reference" href="options.html#option-vim.autocomplete.nvim-cmp.enable"><code class="nixos-option">vim.autocomplete.nvim-cmp.enable</code></a>.</li>
<li>Deprecate legacy Vimsnip in favor of Luasnip, and integrate
friendly-snippets for bundled snippets.
<a class="option-reference" href="options.html#option-vim-snippets-luasnip-enable"><code class="nixos-option">vim.snippets.luasnip.enable</code></a> can be used to toggle Luasnip.</li>
<a class="option-reference" href="options.html#option-vim.snippets.luasnip.enable"><code class="nixos-option">vim.snippets.luasnip.enable</code></a> can be used to toggle Luasnip.</li>
<li>Add sorting function options for completion sources under
<a class="option-reference" href="options.html#option-vim-autocomplete-nvim-cmp-setupOpts-sorting-comparators"><code class="nixos-option">vim.autocomplete.nvim-cmp.setupOpts.sorting.comparators</code></a></li>
<a class="option-reference" href="options.html#option-vim.autocomplete.nvim-cmp.setupOpts.sorting.comparators"><code class="nixos-option">vim.autocomplete.nvim-cmp.setupOpts.sorting.comparators</code></a></li>
</ul>
</li>
<li>
@ -1591,11 +1596,11 @@ identical clone.</li>
Lualine. Only <code>vim.ui.breadcrumbs.lualine.winbar</code> is supported for the time
being.</p>
<ul>
<li><a class="option-reference" href="options.html#option-vim-ui-breadcrumbs-lualine-winbar-enable"><code class="nixos-option">vim.ui.breadcrumbs.lualine.winbar.enable</code></a> has been added to allow
<li><a class="option-reference" href="options.html#option-vim.ui.breadcrumbs.lualine.winbar.enable"><code class="nixos-option">vim.ui.breadcrumbs.lualine.winbar.enable</code></a> has been added to allow
controlling the default behaviour of the <code>nvim-navic</code> component on Lualine,
which used to occupy <code>winbar.lualine_c</code> as long as breadcrumbs are enabled.</li>
<li><code>vim.ui.breadcrumbs.alwaysRender</code> has been renamed to
<a class="option-reference" href="options.html#option-vim-ui-breadcrumbs-lualine-winbar-alwaysRender"><code class="nixos-option">vim.ui.breadcrumbs.lualine.winbar.alwaysRender</code></a> to be conform to
<a class="option-reference" href="options.html#option-vim.ui.breadcrumbs.lualine.winbar.alwaysRender"><code class="nixos-option">vim.ui.breadcrumbs.lualine.winbar.alwaysRender</code></a> to be conform to
the new format.</li>
</ul>
</li>
@ -1608,11 +1613,11 @@ server and make it default.</p>
additional Python LSP server.</p>
</li>
<li>
<p>Add <a class="option-reference" href="options.html#option-vim-options"><code class="nixos-option">vim.options</code></a> to set <code>vim.o</code> values in in your nvf configuration
<p>Add <a class="option-reference" href="options.html#option-vim.options"><code class="nixos-option">vim.options</code></a> to set <code>vim.o</code> values in in your nvf configuration
without using additional Lua. See option documentation for more details.</p>
</li>
<li>
<p>Add <a class="option-reference" href="options.html#option-vim-dashboard-dashboard-nvim-setupOpts"><code class="nixos-option">vim.dashboard.dashboard-nvim.setupOpts</code></a> to allow user
<p>Add <a class="option-reference" href="options.html#option-vim.dashboard.dashboard-nvim.setupOpts"><code class="nixos-option">vim.dashboard.dashboard-nvim.setupOpts</code></a> to allow user
configuration for <a href="https://github.com/nvimdev/dashboard-nvim">dashboard.nvim</a></p>
</li>
<li>
@ -1623,7 +1628,7 @@ configuration for <a href="https://github.com/nvimdev/dashboard-nvim">dashboard.
</ul>
</li>
<li>
<p>Add <a class="option-reference" href="options.html#option-vim-spellcheck-extraSpellWords"><code class="nixos-option">vim.spellcheck.extraSpellWords</code></a> to allow adding arbitrary
<p>Add <a class="option-reference" href="options.html#option-vim.spellcheck.extraSpellWords"><code class="nixos-option">vim.spellcheck.extraSpellWords</code></a> to allow adding arbitrary
spellfiles to Neovim's runtime with ease.</p>
</li>
<li>
@ -1687,9 +1692,9 @@ the Typst language module.</li>
<p><a href="https://github.com/nezia1">nezia1</a>:</p>
<ul>
<li>Add <a href="https://github.com/biomejs/biome">biome</a> support for Typescript, CSS and
Svelte. Enable them via <a class="option-reference" href="options.html#option-vim-languages-typescript-format-type"><code class="nixos-option">vim.languages.typescript.format.type</code></a>,
<a class="option-reference" href="options.html#option-vim-languages-css-format-type"><code class="nixos-option">vim.languages.css.format.type</code></a> and
<a class="option-reference" href="options.html#option-vim-languages-svelte-format-type"><code class="nixos-option">vim.languages.svelte.format.type</code></a> respectively.</li>
Svelte. Enable them via <a class="option-reference" href="options.html#option-vim.languages.typescript.format.type"><code class="nixos-option">vim.languages.typescript.format.type</code></a>,
<a class="option-reference" href="options.html#option-vim.languages.css.format.type"><code class="nixos-option">vim.languages.css.format.type</code></a> and
<a class="option-reference" href="options.html#option-vim.languages.svelte.format.type"><code class="nixos-option">vim.languages.svelte.format.type</code></a> respectively.</li>
<li>Replace <a href="https://github.com/nix-community/nixpkgs-fmt">nixpkgs-fmt</a> with
<a href="https://github.com/NixOS/nixfmt">nixfmt</a> (nixfmt-rfc-style).</li>
</ul>
@ -1725,7 +1730,7 @@ lot):</p>
</li>
<li>
<p>Added <code>ChatGPT.nvim</code>, which can be enabled with
<a class="option-reference" href="options.html#option-vim-assistant-chatgpt-enable"><code class="nixos-option">vim.assistant.chatgpt.enable</code></a>. Do keep in mind that this option
<a class="option-reference" href="options.html#option-vim.assistant.chatgpt.enable"><code class="nixos-option">vim.assistant.chatgpt.enable</code></a>. Do keep in mind that this option
requires <code>OPENAI_API_KEY</code> environment variable to be set.</p>
</li>
</ul>
@ -1787,7 +1792,7 @@ and also has been removed.</p>
</li>
<li>
<p><code>which-key.nvim</code> categories can now be customized through
<a href="./options.html#option-vim-binds-whichKey-register">vim.binds.whichKey.register</a></p>
<a class="option-reference" href="options.html#option-vim.binds.whichKey.register"><code class="nixos-option">vim.binds.whichKey.register</code></a></p>
</li>
<li>
<p>Added <code>magick</code> to <code>vim.luaPackages</code> for <code>image.nvim</code>.</p>
@ -1827,10 +1832,9 @@ enabled through <code>vim.languages.css</code> and <code>vim.languages.tailwind<
<li>
<p>Lualine module now allows customizing <code>always_divide_middle</code>, <code>ignore_focus</code>
and <code>disabled_filetypes</code> through the new options:
<a href="./options.html#option-vim-statusline-lualine-alwaysDivideMiddle">vim.statusline.lualine.alwaysDivideMiddle</a>,
<a href="./options.html#option-vim-statusline-lualine-ignoreFocus">vim.statusline.lualine.ignoreFocus</a>
and
<a href="./options.html#option-vim-statusline-lualine-disabledFiletypes">vim.statusline.lualine.disabledFiletypes</a>.</p>
<a class="option-reference" href="options.html#option-vim.statusline.lualine.alwaysDivideMiddle"><code class="nixos-option">vim.statusline.lualine.alwaysDivideMiddle</code></a>,
<a class="option-reference" href="options.html#option-vim.statusline.lualine.ignoreFocus"><code class="nixos-option">vim.statusline.lualine.ignoreFocus</code></a> and
<a class="option-reference" href="options.html#option-vim.statusline.lualine.disabledFiletypes"><code class="nixos-option">vim.statusline.lualine.disabledFiletypes</code></a>).</p>
</li>
<li>
<p>Updated all plugin inputs to their latest versions (<strong>21.04.2024</strong>) - this
@ -1870,7 +1874,7 @@ arguments to take <code>luaBefore</code>, <code>luaConfig</code> and <code>luaAf
are then concatted inside a lua block.</p>
</li>
<li>
<p>Added <a class="option-reference" href="options.html#option-vim-luaConfigPre"><code class="nixos-option">vim.luaConfigPre</code></a> and {option} <code>vim-luaConfigPost</code> for
<p>Added <a class="option-reference" href="options.html#option-vim.luaConfigPre"><code class="nixos-option">vim.luaConfigPre</code></a> and {option} <code>vim-luaConfigPost</code> for
inserting verbatim Lua configuration before and after the resolved Lua DAG
respectively. Both of those options take strings as the type, so you may read
the contents of a Lua file from a given path.</p>
@ -1886,7 +1890,7 @@ used <code>vim.spellcheck.vim-dirtytalk</code> aliases to the latter option.</p>
the <code>makeNeovimConfig</code> function under their respective options.</p>
</li>
<li>
<p>Added <a class="option-reference" href="options.html#option-vim-extraPackages"><code class="nixos-option">vim.extraPackages</code></a> for appending additional packages to the
<p>Added <a class="option-reference" href="options.html#option-vim.extraPackages"><code class="nixos-option">vim.extraPackages</code></a> for appending additional packages to the
wrapper PATH, making said packages available while inside the Neovim session.</p>
</li>
<li>
@ -1894,7 +1898,7 @@ wrapper PATH, making said packages available while inside the Neovim session.</p
<code>setupOpts</code> while it is enabled.</p>
</li>
<li>
<p>Added <a class="option-reference" href="options.html#option-vim-notify-nvim-notify-setupOpts-render"><code class="nixos-option">vim.notify.nvim-notify.setupOpts.render</code></a> which takes either a
<p>Added <a class="option-reference" href="options.html#option-vim.notify.nvim-notify.setupOpts.render"><code class="nixos-option">vim.notify.nvim-notify.setupOpts.render</code></a> which takes either a
string of enum, or a Lua function. The default is "compact", but you may
change it according to nvim-notify documentation.</p>
</li>
@ -1920,7 +1924,7 @@ change it according to nvim-notify documentation.</p>
</li>
<li>
<p>Streamlined and simplified extra plugin API with the addition of
<a class="option-reference" href="options.html#option-vim-extraPlugins"><code class="nixos-option">vim.extraPlugins</code></a></p>
<a class="option-reference" href="options.html#option-vim.extraPlugins"><code class="nixos-option">vim.extraPlugins</code></a></p>
</li>
<li>
<p>Allow using command names in place of LSP packages to avoid automatic
@ -1930,7 +1934,7 @@ installation</p>
<p>Add lua LSP and Treesitter support, and neodev.nvim plugin support</p>
</li>
<li>
<p>Add <a class="option-reference" href="options.html#option-vim-lsp-mappings-toggleFormatOnSave"><code class="nixos-option">vim.lsp.mappings.toggleFormatOnSave</code></a> keybind</p>
<p>Add <a class="option-reference" href="options.html#option-vim.lsp.mappings.toggleFormatOnSave"><code class="nixos-option">vim.lsp.mappings.toggleFormatOnSave</code></a> keybind</p>
</li>
</ul>
<p><a href="https://github.com/amanse">amanse</a>:</p>
@ -1971,11 +1975,11 @@ the presence of line numbers</p>
<p>Added GitHub Copilot to nvim-cmp completion sources.</p>
</li>
<li>
<p>Added <a class="option-reference" href="options.html#option-vim-ui-borders-enable"><code class="nixos-option">vim.ui.borders.enable</code></a> for global and individual plugin border
<p>Added <a class="option-reference" href="options.html#option-vim.ui.borders.enable"><code class="nixos-option">vim.ui.borders.enable</code></a> for global and individual plugin border
configuration.</p>
</li>
<li>
<p>LSP integrated breadcrumbs with <a class="option-reference" href="options.html#option-vim-ui-breadcrumbs-enable"><code class="nixos-option">vim.ui.breadcrumbs.enable</code></a> through
<p>LSP integrated breadcrumbs with <a class="option-reference" href="options.html#option-vim.ui.breadcrumbs.enable"><code class="nixos-option">vim.ui.breadcrumbs.enable</code></a> through
nvim-navic</p>
</li>
<li>
@ -1990,7 +1994,7 @@ enabled if navic is enabled)</p>
</li>
<li>
<p>Added support for <code>statix</code> and <code>deadnix</code> through
<a class="option-reference" href="options.html#option-vim-languages-nix-extraDiagnostics-types"><code class="nixos-option">vim.languages.nix.extraDiagnostics.types</code></a></p>
<a class="option-reference" href="options.html#option-vim.languages.nix.extraDiagnostics.types"><code class="nixos-option">vim.languages.nix.extraDiagnostics.types</code></a></p>
</li>
<li>
<p>Added <code>lsp_lines</code> plugin for showing diagnostic messages</p>
@ -2000,7 +2004,7 @@ enabled if navic is enabled)</p>
</li>
<li>
<p>The package used for neovim is now customizable by the user, using
<a class="option-reference" href="options.html#option-vim-package"><code class="nixos-option">vim.package</code></a>. For best results, always use an unwrapped package</p>
<a class="option-reference" href="options.html#option-vim.package"><code class="nixos-option">vim.package</code></a>. For best results, always use an unwrapped package</p>
</li>
<li>
<p>Added highlight-undo plugin for highlighting undo/redo targets</p>
@ -2221,7 +2225,7 @@ attempt to use the Zig overlay to return Darwin support.</p>
</li>
<li>
<p>Treesitter grammars are now configurable with
<a class="option-reference" href="options.html#option-vim-treesitter-grammars"><code class="nixos-option">vim.treesitter.grammars</code></a>. Utilizes the nixpkgs <code>nvim-treesitter</code>
<a class="option-reference" href="options.html#option-vim.treesitter.grammars"><code class="nixos-option">vim.treesitter.grammars</code></a>. Utilizes the nixpkgs <code>nvim-treesitter</code>
plugin rather than a custom input in order to take advantage of build support
of pinned versions. See <a href="https://discourse.nixos.org/t/psa-if-you-are-on-unstable-try-out-nvim-treesitter-withallgrammars/23321?u=snowytrees">discourse</a> for more information. Packages can be
found under the <code>pkgs.vimPlugins.nvim-treesitter.builtGrammars</code> attribute.
@ -2233,13 +2237,13 @@ which do not have a language section are not included anymore: <strong>comment</
<li>
<p>A new section has been added for language support: <code>vim.languages.&lt;language&gt;</code>.</p>
<ul>
<li>The options <code>enableLSP</code> <a class="option-reference" href="options.html#option-vim-languages-enableTreesitter"><code class="nixos-option">vim.languages.enableTreesitter</code></a>, etc. will
<li>The options <code>enableLSP</code> <a class="option-reference" href="options.html#option-vim.languages.enableTreesitter"><code class="nixos-option">vim.languages.enableTreesitter</code></a>, etc. will
enable the respective section for all languages that have been enabled.</li>
<li>All LSP languages have been moved here</li>
<li><code>plantuml</code> and <code>markdown</code> have been moved here</li>
<li>A new section has been added for <code>html</code>. The old
<code>vim.treesitter.autotagHtml</code> can be found at
<a class="option-reference" href="options.html#option-vim-languages-html-treesitter-autotagHtml"><code class="nixos-option">vim.languages.html.treesitter.autotagHtml</code></a>.</li>
<a class="option-reference" href="options.html#option-vim.languages.html.treesitter.autotagHtml"><code class="nixos-option">vim.languages.html.treesitter.autotagHtml</code></a>.</li>
</ul>
</li>
<li>
@ -2250,7 +2254,7 @@ Gitsigns' code actions.</p>
<p>Removed the plugins document in the docs. Was too unwieldy to keep updated.</p>
</li>
<li>
<p><code>vim.visual.lspkind</code> has been moved to <a class="option-reference" href="options.html#option-vim-lsp-lspkind-enable"><code class="nixos-option">vim.lsp.lspkind.enable</code></a></p>
<p><code>vim.visual.lspkind</code> has been moved to <a class="option-reference" href="options.html#option-vim.lsp.lspkind.enable"><code class="nixos-option">vim.lsp.lspkind.enable</code></a></p>
</li>
<li>
<p>Improved handling of completion formatting. When setting
@ -2264,7 +2268,7 @@ by using <code>null</code> rather than <code>""</code> now.</p>
</li>
<li>
<p>Transparency has been made optional and has been disabled by default.
<a class="option-reference" href="options.html#option-vim-theme-transparent"><code class="nixos-option">vim.theme.transparent</code></a> option can be used to enable or disable
<a class="option-reference" href="options.html#option-vim.theme.transparent"><code class="nixos-option">vim.theme.transparent</code></a> option can be used to enable or disable
transparency for your configuration.</p>
</li>
<li>
@ -2363,7 +2367,7 @@ longer defined. If you use hare and would like it added back, please file an
issue.</p>
</li>
<li>
<p><a class="option-reference" href="options.html#option-vim-startPlugins"><code class="nixos-option">vim.startPlugins</code></a> &amp; {option} <code>vim-optPlugins</code> are now an enum of
<p><a class="option-reference" href="options.html#option-vim.startPlugins"><code class="nixos-option">vim.startPlugins</code></a> &amp; <a class="option-reference" href="options.html#option-vim.optPlugins"><code class="nixos-option">vim.optPlugins</code></a> are now an enum of
<code>string</code> for options sourced from the flake inputs. Users can still provide
vim plugin packages.</p>
<ul>
@ -2379,14 +2383,14 @@ longer required. See the manual for the new way to configuration.</p>
<ul>
<li>
<p>Treesitter grammars are now configurable with
<a class="option-reference" href="options.html#option-vim-treesitter-grammars"><code class="nixos-option">vim.treesitter.grammars</code></a>. Utilizes the nixpkgs <code>nvim-treesitter</code>
<a class="option-reference" href="options.html#option-vim.treesitter.grammars"><code class="nixos-option">vim.treesitter.grammars</code></a>. Utilizes the nixpkgs <code>nvim-treesitter</code>
plugin rather than a custom input in order to take advantage of build support
of pinned versions. See the <a href="https://discourse.nixos.org/t/psa-if-you-are-on-unstable-try-out-nvim-treesitter-withallgrammars/23321?u=snowytrees">relevant discourse post</a> for more information.
Packages can be found under the <code>vimPlugins.nvim-treesitter.builtGrammars</code>
namespace.</p>
</li>
<li>
<p><code>vim.configRC</code> and <a class="option-reference" href="options.html#option-vim-luaConfigRC"><code class="nixos-option">vim.luaConfigRC</code></a> are now of type DAG lines. This
<p><code>vim.configRC</code> and <a class="option-reference" href="options.html#option-vim.luaConfigRC"><code class="nixos-option">vim.luaConfigRC</code></a> are now of type DAG lines. This
allows for ordering of the config. Usage is the same is in home-manager's
<code>home.activation</code> option.</p>
</li>
@ -2395,8 +2399,8 @@ allows for ordering of the config. Usage is the same is in home-manager's
<p><a href="https://github.com/MoritzBoehme">MoritzBoehme</a>:</p>
<ul>
<li><code>catppuccin</code> theme is now available as a neovim theme
<a class="option-reference" href="options.html#option-vim-theme-style"><code class="nixos-option">vim.theme.style</code></a> and Lualine theme
<a class="option-reference" href="options.html#option-vim-statusline-lualine-theme"><code class="nixos-option">vim.statusline.lualine.theme</code></a>.</li>
<a class="option-reference" href="options.html#option-vim.theme.style"><code class="nixos-option">vim.theme.style</code></a> and Lualine theme
<a class="option-reference" href="options.html#option-vim.statusline.lualine.theme"><code class="nixos-option">vim.statusline.lualine.theme</code></a>.</li>
</ul>
</body></html></main>
</div>

View file

@ -5,11 +5,16 @@
<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");
try {
if (localStorage.getItem("sidebar-collapsed") === "true") {
document.documentElement.classList.add("sidebar-collapsed");
}
} catch (e) {
// localStorage unavailable
}
})();
</script>
@ -41,8 +46,20 @@
</nav>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search..." />
<div id="search-results" class="search-results"></div>
<input
type="search"
id="search-input"
placeholder="Search..."
aria-label="Search"
autocomplete="off"
/>
<div
id="search-results"
class="search-results"
role="region"
aria-live="polite"
aria-label="Search results"
></div>
</div>
</header>
@ -85,13 +102,26 @@
<div class="search-page">
<div class="search-form">
<input
type="text"
type="search"
id="search-page-input"
placeholder="Search..."
aria-label="Search"
autocomplete="off"
autofocus
/>
</div>
<div id="search-page-results" class="search-page-results"></div>
<div class="search-keyboard-hints" role="note" aria-label="Keyboard shortcuts">
<span class="hint-item"><kbd></kbd> <kbd></kbd> to navigate</span>
<span class="hint-item"><kbd>Enter</kbd> to select</span>
<span class="hint-item"><kbd>Esc</kbd> to clear</span>
</div>
<div
id="search-page-results"
class="search-page-results"
role="region"
aria-live="polite"
aria-label="Search results"
></div>
</div>
<div class="footnotes-container">
<!-- Footnotes will be appended here -->

File diff suppressed because one or more lines are too long