mirror of
https://github.com/NotAShelf/nvf.git
synced 2026-04-26 19:37:36 +00:00
deploy: 35a64b0c64
This commit is contained in:
parent
0f6f61fa3d
commit
d39bd7cecd
64 changed files with 8748 additions and 249256 deletions
504
assets/main.js
504
assets/main.js
|
|
@ -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">×</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">×</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;
|
||||
|
||||
// Create a case-insensitive regex pattern
|
||||
const pattern = terms
|
||||
.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||
.join("|");
|
||||
const regex = new RegExp(`(${pattern})`, "gi");
|
||||
|
||||
// Elements to skip highlighting
|
||||
const skipTags = new Set(["SCRIPT", "STYLE", "CODE", "PRE", "MARK"]);
|
||||
|
||||
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>',
|
||||
);
|
||||
|
||||
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");
|
||||
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
|
||||
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",
|
||||
);
|
||||
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));
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
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
492
assets/search.js
492
assets/search.js
|
|
@ -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)) {
|
||||
// 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.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(
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
428
assets/style.css
428
assets/style.css
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
163
configuring.html
163
configuring.html
File diff suppressed because one or more lines are too long
|
|
@ -1,740 +0,0 @@
|
|||
// Polyfill for requestIdleCallback for Safari and unsupported browsers
|
||||
if (typeof window.requestIdleCallback === "undefined") {
|
||||
window.requestIdleCallback = function (cb) {
|
||||
const start = Date.now();
|
||||
const idlePeriod = 50;
|
||||
return setTimeout(function () {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
timeRemaining: function () {
|
||||
return Math.max(0, idlePeriod - (Date.now() - start));
|
||||
},
|
||||
});
|
||||
}, 1);
|
||||
};
|
||||
window.cancelIdleCallback = function (id) {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}
|
||||
|
||||
// Create mobile elements if they don't exist
|
||||
function createMobileElements() {
|
||||
// Create mobile sidebar FAB
|
||||
const mobileFab = document.createElement("button");
|
||||
mobileFab.className = "mobile-sidebar-fab";
|
||||
mobileFab.setAttribute("aria-label", "Toggle sidebar menu");
|
||||
mobileFab.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Only show FAB on mobile (max-width: 800px)
|
||||
function updateFabVisibility() {
|
||||
if (window.innerWidth > 800) {
|
||||
if (mobileFab.parentNode) mobileFab.parentNode.removeChild(mobileFab);
|
||||
} else {
|
||||
if (!document.body.contains(mobileFab)) {
|
||||
document.body.appendChild(mobileFab);
|
||||
}
|
||||
mobileFab.style.display = "flex";
|
||||
}
|
||||
}
|
||||
updateFabVisibility();
|
||||
window.addEventListener("resize", updateFabVisibility);
|
||||
|
||||
// Create mobile sidebar container
|
||||
const mobileContainer = document.createElement("div");
|
||||
mobileContainer.className = "mobile-sidebar-container";
|
||||
mobileContainer.innerHTML = `
|
||||
<div class="mobile-sidebar-handle">
|
||||
<div class="mobile-sidebar-dragger"></div>
|
||||
</div>
|
||||
<div class="mobile-sidebar-content">
|
||||
<!-- Sidebar content will be cloned here -->
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create mobile search popup
|
||||
const mobileSearchPopup = document.createElement("div");
|
||||
mobileSearchPopup.id = "mobile-search-popup";
|
||||
mobileSearchPopup.className = "mobile-search-popup";
|
||||
mobileSearchPopup.innerHTML = `
|
||||
<div class="mobile-search-container">
|
||||
<div class="mobile-search-header">
|
||||
<input type="text" id="mobile-search-input" placeholder="Search..." />
|
||||
<button id="close-mobile-search" class="close-mobile-search" aria-label="Close search">×</button>
|
||||
</div>
|
||||
<div id="mobile-search-results" class="mobile-search-results"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert at end of body so it is not affected by .container flex or stacking context
|
||||
document.body.appendChild(mobileContainer);
|
||||
document.body.appendChild(mobileSearchPopup);
|
||||
|
||||
// Immediately populate mobile sidebar content if desktop sidebar exists
|
||||
const desktopSidebar = document.querySelector(".sidebar");
|
||||
const mobileSidebarContent = mobileContainer.querySelector(
|
||||
".mobile-sidebar-content",
|
||||
);
|
||||
if (desktopSidebar && mobileSidebarContent) {
|
||||
mobileSidebarContent.innerHTML = desktopSidebar.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize collapsible sidebar sections with state persistence
|
||||
function initCollapsibleSections() {
|
||||
// Target sections in both desktop and mobile sidebars
|
||||
const sections = document.querySelectorAll(
|
||||
".sidebar .sidebar-section, .mobile-sidebar-content .sidebar-section",
|
||||
);
|
||||
|
||||
sections.forEach((section) => {
|
||||
const sectionId = section.dataset.section;
|
||||
if (!sectionId) return;
|
||||
|
||||
const storageKey = `sidebar-section-${sectionId}`;
|
||||
const savedState = localStorage.getItem(storageKey);
|
||||
|
||||
// Restore saved state (default is open)
|
||||
if (savedState === "closed") {
|
||||
section.removeAttribute("open");
|
||||
}
|
||||
|
||||
// Save state on toggle and sync between desktop/mobile
|
||||
section.addEventListener("toggle", () => {
|
||||
localStorage.setItem(storageKey, section.open ? "open" : "closed");
|
||||
|
||||
// Sync state between desktop and mobile versions
|
||||
const allWithSameSection = document.querySelectorAll(
|
||||
`.sidebar-section[data-section="${sectionId}"]`,
|
||||
);
|
||||
allWithSameSection.forEach((el) => {
|
||||
if (el !== section) {
|
||||
el.open = section.open;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize scroll spy
|
||||
function initScrollSpy() {
|
||||
const pageToc = document.querySelector(".page-toc");
|
||||
if (!pageToc) return;
|
||||
|
||||
const tocLinks = pageToc.querySelectorAll(".page-toc-list a");
|
||||
const content = document.querySelector(".content");
|
||||
if (!tocLinks.length || !content) return;
|
||||
|
||||
const headings = Array.from(
|
||||
content.querySelectorAll("h1[id], h2[id], h3[id]"),
|
||||
);
|
||||
|
||||
if (!headings.length) return;
|
||||
|
||||
// Build a map of heading IDs to TOC links for quick lookup
|
||||
const linkMap = new Map();
|
||||
tocLinks.forEach((link) => {
|
||||
const href = link.getAttribute("href");
|
||||
if (href && href.startsWith("#")) {
|
||||
linkMap.set(href.slice(1), link);
|
||||
}
|
||||
});
|
||||
|
||||
let activeLink = null;
|
||||
|
||||
// Update active link based on scroll position
|
||||
function updateActiveLink() {
|
||||
const threshold = 120; // threshold from the top of the viewport
|
||||
|
||||
let currentHeading = null;
|
||||
|
||||
// Find the last heading that is at or above the threshold
|
||||
for (const heading of headings) {
|
||||
const rect = heading.getBoundingClientRect();
|
||||
if (rect.top <= threshold) {
|
||||
currentHeading = heading;
|
||||
}
|
||||
}
|
||||
|
||||
// If no heading is above threshold, use first heading if it's in view
|
||||
if (!currentHeading && headings.length > 0) {
|
||||
const firstRect = headings[0].getBoundingClientRect();
|
||||
if (firstRect.top < window.innerHeight) {
|
||||
currentHeading = headings[0];
|
||||
}
|
||||
}
|
||||
|
||||
const newLink = currentHeading ? linkMap.get(currentHeading.id) : null;
|
||||
|
||||
if (newLink !== activeLink) {
|
||||
if (activeLink) {
|
||||
activeLink.classList.remove("active");
|
||||
}
|
||||
if (newLink) {
|
||||
newLink.classList.add("active");
|
||||
}
|
||||
activeLink = newLink;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll event handler
|
||||
let ticking = false;
|
||||
function onScroll() {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
updateActiveLink();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
// Also update on hash change (direct link navigation)
|
||||
window.addEventListener("hashchange", () => {
|
||||
requestAnimationFrame(updateActiveLink);
|
||||
});
|
||||
|
||||
// Set initial active state after a small delay to ensure
|
||||
// browser has completed any hash-based scrolling
|
||||
setTimeout(updateActiveLink, 100);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Apply sidebar state immediately before DOM rendering
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
document.body.classList.add("sidebar-collapsed");
|
||||
}
|
||||
|
||||
if (!document.querySelector(".mobile-sidebar-fab")) {
|
||||
createMobileElements();
|
||||
}
|
||||
|
||||
// Initialize collapsible sidebar sections
|
||||
// after mobile elements are created
|
||||
initCollapsibleSections();
|
||||
|
||||
// Initialize scroll spy for page TOC
|
||||
initScrollSpy();
|
||||
|
||||
// Desktop Sidebar Toggle
|
||||
const sidebarToggle = document.querySelector(".sidebar-toggle");
|
||||
|
||||
// On page load, sync the state from `documentElement` to `body`
|
||||
if (document.documentElement.classList.contains("sidebar-collapsed")) {
|
||||
document.body.classList.add("sidebar-collapsed");
|
||||
}
|
||||
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener("click", function () {
|
||||
// Toggle on both elements for consistency
|
||||
document.documentElement.classList.toggle("sidebar-collapsed");
|
||||
document.body.classList.toggle("sidebar-collapsed");
|
||||
|
||||
// Use documentElement to check state and save to localStorage
|
||||
const isCollapsed = document.documentElement.classList.contains(
|
||||
"sidebar-collapsed",
|
||||
);
|
||||
localStorage.setItem("sidebar-collapsed", isCollapsed);
|
||||
});
|
||||
}
|
||||
|
||||
// Make headings clickable for anchor links
|
||||
const content = document.querySelector(".content");
|
||||
if (content) {
|
||||
const headings = content.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
||||
|
||||
headings.forEach(function (heading) {
|
||||
// Generate a valid, unique ID for each heading
|
||||
if (!heading.id) {
|
||||
let baseId = heading.textContent
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-_]/g, "") // remove invalid chars
|
||||
.replace(/^[^a-z]+/, "") // remove leading non-letters
|
||||
.replace(/[\s-_]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") // trim leading/trailing dashes
|
||||
.trim();
|
||||
if (!baseId) {
|
||||
baseId = "section";
|
||||
}
|
||||
let id = baseId;
|
||||
let counter = 1;
|
||||
while (document.getElementById(id)) {
|
||||
id = `${baseId}-${counter++}`;
|
||||
}
|
||||
heading.id = id;
|
||||
}
|
||||
|
||||
// Make the entire heading clickable
|
||||
heading.addEventListener("click", function () {
|
||||
const id = this.id;
|
||||
history.pushState(null, null, "#" + id);
|
||||
|
||||
// Scroll with offset
|
||||
const offset = this.getBoundingClientRect().top + window.scrollY - 80;
|
||||
window.scrollTo({
|
||||
top: offset,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Process footnotes
|
||||
if (content) {
|
||||
const footnoteContainer = document.querySelector(".footnotes-container");
|
||||
|
||||
// Find all footnote references and create a footnotes section
|
||||
const footnoteRefs = content.querySelectorAll('a[href^="#fn"]');
|
||||
if (footnoteRefs.length > 0) {
|
||||
const footnotesDiv = document.createElement("div");
|
||||
footnotesDiv.className = "footnotes";
|
||||
|
||||
const footnotesHeading = document.createElement("h2");
|
||||
footnotesHeading.textContent = "Footnotes";
|
||||
footnotesDiv.appendChild(footnotesHeading);
|
||||
|
||||
const footnotesList = document.createElement("ol");
|
||||
footnoteContainer.appendChild(footnotesDiv);
|
||||
footnotesDiv.appendChild(footnotesList);
|
||||
|
||||
// Add footnotes
|
||||
document.querySelectorAll(".footnote").forEach((footnote) => {
|
||||
const id = footnote.id;
|
||||
const content = footnote.innerHTML;
|
||||
|
||||
const li = document.createElement("li");
|
||||
li.id = id;
|
||||
li.innerHTML = content;
|
||||
|
||||
// Add backlink
|
||||
const backlink = document.createElement("a");
|
||||
backlink.href = "#fnref:" + id.replace("fn:", "");
|
||||
backlink.className = "footnote-backlink";
|
||||
backlink.textContent = "↩";
|
||||
li.appendChild(backlink);
|
||||
|
||||
footnotesList.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Copy link functionality
|
||||
document.querySelectorAll(".copy-link").forEach(function (copyLink) {
|
||||
copyLink.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Get option ID from parent element
|
||||
const option = copyLink.closest(".option");
|
||||
const optionId = option.id;
|
||||
|
||||
// Create URL with hash
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = optionId;
|
||||
|
||||
// Copy to clipboard
|
||||
navigator.clipboard
|
||||
.writeText(url.toString())
|
||||
.then(function () {
|
||||
// Show feedback
|
||||
const feedback = copyLink.nextElementSibling;
|
||||
feedback.style.display = "inline";
|
||||
|
||||
// Hide after 2 seconds
|
||||
setTimeout(function () {
|
||||
feedback.style.display = "none";
|
||||
}, 2000);
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("Could not copy link: ", err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Handle initial hash navigation
|
||||
function scrollToElement(element) {
|
||||
if (element) {
|
||||
const offset = element.getBoundingClientRect().top + window.scrollY - 80;
|
||||
window.scrollTo({
|
||||
top: offset,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (window.location.hash) {
|
||||
const targetElement = document.querySelector(window.location.hash);
|
||||
if (targetElement) {
|
||||
setTimeout(() => scrollToElement(targetElement), 0);
|
||||
// Add highlight class for options page
|
||||
if (targetElement.classList.contains("option")) {
|
||||
targetElement.classList.add("highlight");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile Sidebar Functionality
|
||||
const mobileSidebarContainer = document.querySelector(
|
||||
".mobile-sidebar-container",
|
||||
);
|
||||
const mobileSidebarFab = document.querySelector(".mobile-sidebar-fab");
|
||||
const mobileSidebarHandle = document.querySelector(".mobile-sidebar-handle");
|
||||
|
||||
// Always set up FAB if it exists
|
||||
if (mobileSidebarFab && mobileSidebarContainer) {
|
||||
const openMobileSidebar = () => {
|
||||
mobileSidebarContainer.classList.add("active");
|
||||
mobileSidebarFab.setAttribute("aria-expanded", "true");
|
||||
mobileSidebarContainer.setAttribute("aria-hidden", "false");
|
||||
mobileSidebarFab.classList.add("fab-hidden"); // hide FAB when drawer is open
|
||||
};
|
||||
|
||||
const closeMobileSidebar = () => {
|
||||
mobileSidebarContainer.classList.remove("active");
|
||||
mobileSidebarFab.setAttribute("aria-expanded", "false");
|
||||
mobileSidebarContainer.setAttribute("aria-hidden", "true");
|
||||
mobileSidebarFab.classList.remove("fab-hidden"); // Show FAB when drawer is closed
|
||||
};
|
||||
|
||||
mobileSidebarFab.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (mobileSidebarContainer.classList.contains("active")) {
|
||||
closeMobileSidebar();
|
||||
} else {
|
||||
openMobileSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Only set up drag functionality if handle exists
|
||||
if (mobileSidebarHandle) {
|
||||
// Drag functionality
|
||||
let isDragging = false;
|
||||
let startY = 0;
|
||||
let startHeight = 0;
|
||||
|
||||
// Cleanup function for drag interruption
|
||||
function cleanupDrag() {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
mobileSidebarHandle.style.cursor = "grab";
|
||||
document.body.style.userSelect = "";
|
||||
}
|
||||
}
|
||||
|
||||
mobileSidebarHandle.addEventListener("mousedown", (e) => {
|
||||
isDragging = true;
|
||||
startY = e.pageY;
|
||||
startHeight = mobileSidebarContainer.offsetHeight;
|
||||
mobileSidebarHandle.style.cursor = "grabbing";
|
||||
document.body.style.userSelect = "none"; // prevent text selection
|
||||
});
|
||||
|
||||
mobileSidebarHandle.addEventListener("touchstart", (e) => {
|
||||
isDragging = true;
|
||||
startY = e.touches[0].pageY;
|
||||
startHeight = mobileSidebarContainer.offsetHeight;
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
if (!isDragging) return;
|
||||
const deltaY = startY - e.pageY;
|
||||
const newHeight = startHeight + deltaY;
|
||||
const vh = window.innerHeight;
|
||||
const minHeight = vh * 0.15;
|
||||
const maxHeight = vh * 0.9;
|
||||
|
||||
if (newHeight >= minHeight && newHeight <= maxHeight) {
|
||||
mobileSidebarContainer.style.height = `${newHeight}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("touchmove", (e) => {
|
||||
if (!isDragging) return;
|
||||
const deltaY = startY - e.touches[0].pageY;
|
||||
const newHeight = startHeight + deltaY;
|
||||
const vh = window.innerHeight;
|
||||
const minHeight = vh * 0.15;
|
||||
const maxHeight = vh * 0.9;
|
||||
|
||||
if (newHeight >= minHeight && newHeight <= maxHeight) {
|
||||
mobileSidebarContainer.style.height = `${newHeight}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mouseup", cleanupDrag);
|
||||
document.addEventListener("touchend", cleanupDrag);
|
||||
window.addEventListener("blur", cleanupDrag);
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (document.hidden) cleanupDrag();
|
||||
});
|
||||
}
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener("click", (event) => {
|
||||
if (
|
||||
mobileSidebarContainer.classList.contains("active") &&
|
||||
!mobileSidebarContainer.contains(event.target) &&
|
||||
!mobileSidebarFab.contains(event.target)
|
||||
) {
|
||||
closeMobileSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on escape key
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (
|
||||
event.key === "Escape" &&
|
||||
mobileSidebarContainer.classList.contains("active")
|
||||
) {
|
||||
closeMobileSidebar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Options filter functionality
|
||||
const optionsFilter = document.getElementById("options-filter");
|
||||
if (optionsFilter) {
|
||||
const optionsContainer = document.querySelector(".options-container");
|
||||
if (!optionsContainer) return;
|
||||
|
||||
// Only inject the style if it doesn't already exist
|
||||
if (!document.head.querySelector("style[data-options-hidden]")) {
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.setAttribute("data-options-hidden", "");
|
||||
styleEl.textContent = ".option-hidden{display:none!important}";
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
// Create filter results counter
|
||||
const filterResults = document.createElement("div");
|
||||
filterResults.className = "filter-results";
|
||||
optionsFilter.parentNode.insertBefore(
|
||||
filterResults,
|
||||
optionsFilter.nextSibling,
|
||||
);
|
||||
|
||||
// Detect if we're on a mobile device
|
||||
const isMobile = window.innerWidth < 768 ||
|
||||
/Mobi|Android/i.test(navigator.userAgent);
|
||||
|
||||
// Cache all option elements and their searchable content
|
||||
const options = Array.from(document.querySelectorAll(".option"));
|
||||
const totalCount = options.length;
|
||||
|
||||
// Store the original order of option elements
|
||||
const originalOptionOrder = options.slice();
|
||||
|
||||
// Pre-process and optimize searchable content
|
||||
const optionsData = options.map((option) => {
|
||||
const nameElem = option.querySelector(".option-name");
|
||||
const descriptionElem = option.querySelector(".option-description");
|
||||
const id = option.id ? option.id.toLowerCase() : "";
|
||||
const name = nameElem ? nameElem.textContent.toLowerCase() : "";
|
||||
const description = descriptionElem
|
||||
? descriptionElem.textContent.toLowerCase()
|
||||
: "";
|
||||
|
||||
// Extract keywords for faster searching
|
||||
const keywords = (id + " " + name + " " + description)
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((word) => word.length > 1);
|
||||
|
||||
return {
|
||||
element: option,
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
keywords,
|
||||
searchText: (id + " " + name + " " + description).toLowerCase(),
|
||||
};
|
||||
});
|
||||
|
||||
// Chunk size and rendering variables
|
||||
const CHUNK_SIZE = isMobile ? 15 : 40;
|
||||
let pendingRender = null;
|
||||
let currentChunk = 0;
|
||||
let itemsToProcess = [];
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Process options in chunks to prevent UI freezing
|
||||
function processNextChunk() {
|
||||
const startIdx = currentChunk * CHUNK_SIZE;
|
||||
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
|
||||
|
||||
if (startIdx < itemsToProcess.length) {
|
||||
// Process current chunk
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
const item = itemsToProcess[i];
|
||||
if (item.visible) {
|
||||
item.element.classList.remove("option-hidden");
|
||||
} else {
|
||||
item.element.classList.add("option-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
currentChunk++;
|
||||
pendingRender = requestAnimationFrame(processNextChunk);
|
||||
} else {
|
||||
// Finished processing all chunks
|
||||
pendingRender = null;
|
||||
currentChunk = 0;
|
||||
itemsToProcess = [];
|
||||
|
||||
// Update counter at the very end for best performance
|
||||
if (filterResults.visibleCount !== undefined) {
|
||||
if (filterResults.visibleCount < totalCount) {
|
||||
filterResults.textContent =
|
||||
`Showing ${filterResults.visibleCount} of ${totalCount} options`;
|
||||
filterResults.style.display = "block";
|
||||
} else {
|
||||
filterResults.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterOptions() {
|
||||
const searchTerm = optionsFilter.value.toLowerCase().trim();
|
||||
|
||||
if (pendingRender) {
|
||||
cancelAnimationFrame(pendingRender);
|
||||
pendingRender = null;
|
||||
}
|
||||
currentChunk = 0;
|
||||
itemsToProcess = [];
|
||||
|
||||
if (searchTerm === "") {
|
||||
// Restore original DOM order when filter is cleared
|
||||
const fragment = document.createDocumentFragment();
|
||||
originalOptionOrder.forEach((option) => {
|
||||
option.classList.remove("option-hidden");
|
||||
fragment.appendChild(option);
|
||||
});
|
||||
optionsContainer.appendChild(fragment);
|
||||
filterResults.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const searchTerms = searchTerm
|
||||
.split(/\s+/)
|
||||
.filter((term) => term.length > 0);
|
||||
let visibleCount = 0;
|
||||
|
||||
const titleMatches = [];
|
||||
const descMatches = [];
|
||||
optionsData.forEach((data) => {
|
||||
let isTitleMatch = false;
|
||||
let isDescMatch = false;
|
||||
if (searchTerms.length === 1) {
|
||||
const term = searchTerms[0];
|
||||
isTitleMatch = data.name.includes(term);
|
||||
isDescMatch = !isTitleMatch && data.description.includes(term);
|
||||
} else {
|
||||
isTitleMatch = searchTerms.every((term) => data.name.includes(term));
|
||||
isDescMatch = !isTitleMatch &&
|
||||
searchTerms.every((term) => data.description.includes(term));
|
||||
}
|
||||
if (isTitleMatch) {
|
||||
titleMatches.push(data);
|
||||
} else if (isDescMatch) {
|
||||
descMatches.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
if (searchTerms.length === 1) {
|
||||
const term = searchTerms[0];
|
||||
titleMatches.sort(
|
||||
(a, b) => a.name.indexOf(term) - b.name.indexOf(term),
|
||||
);
|
||||
descMatches.sort(
|
||||
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
|
||||
);
|
||||
}
|
||||
|
||||
itemsToProcess = [];
|
||||
titleMatches.forEach((data) => {
|
||||
visibleCount++;
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
});
|
||||
descMatches.forEach((data) => {
|
||||
visibleCount++;
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
});
|
||||
optionsData.forEach((data) => {
|
||||
if (!itemsToProcess.some((item) => item.element === data.element)) {
|
||||
itemsToProcess.push({ element: data.element, visible: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Reorder DOM so all title matches, then desc matches, then hidden
|
||||
const fragment = document.createDocumentFragment();
|
||||
itemsToProcess.forEach((item) => {
|
||||
fragment.appendChild(item.element);
|
||||
});
|
||||
optionsContainer.appendChild(fragment);
|
||||
|
||||
filterResults.visibleCount = visibleCount;
|
||||
pendingRender = requestAnimationFrame(processNextChunk);
|
||||
}
|
||||
|
||||
// Use different debounce times for desktop vs mobile
|
||||
const debouncedFilter = debounce(filterOptions, isMobile ? 200 : 100);
|
||||
|
||||
// Set up event listeners
|
||||
optionsFilter.addEventListener("input", debouncedFilter);
|
||||
optionsFilter.addEventListener("change", filterOptions);
|
||||
|
||||
// Allow clearing with Escape key
|
||||
optionsFilter.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") {
|
||||
optionsFilter.value = "";
|
||||
filterOptions();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle visibility changes
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (!document.hidden && optionsFilter.value) {
|
||||
filterOptions();
|
||||
}
|
||||
});
|
||||
|
||||
// Initially trigger filter if there's a value
|
||||
if (optionsFilter.value) {
|
||||
filterOptions();
|
||||
}
|
||||
|
||||
// Pre-calculate heights for smoother scrolling
|
||||
if (isMobile && totalCount > 50) {
|
||||
requestIdleCallback(() => {
|
||||
const sampleOption = options[0];
|
||||
if (sampleOption) {
|
||||
const height = sampleOption.offsetHeight;
|
||||
if (height > 0) {
|
||||
options.forEach((opt) => {
|
||||
opt.style.containIntrinsicSize = `0 ${height}px`;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,298 +0,0 @@
|
|||
const isWordBoundary = (char) =>
|
||||
/[A-Z]/.test(char) || /[-_\/.]/.test(char) || /\s/.test(char);
|
||||
|
||||
const isCaseTransition = (prev, curr) => {
|
||||
const prevIsUpper = prev.toLowerCase() !== prev;
|
||||
const currIsUpper = curr.toLowerCase() !== curr;
|
||||
return (
|
||||
prevIsUpper && currIsUpper && prev.toLowerCase() !== curr.toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
const findBestSubsequenceMatch = (query, target) => {
|
||||
const n = query.length;
|
||||
const m = target.length;
|
||||
|
||||
if (n === 0 || m === 0) return null;
|
||||
|
||||
const positions = [];
|
||||
|
||||
const memo = new Map();
|
||||
const key = (qIdx, tIdx, gap) => `${qIdx}:${tIdx}:${gap}`;
|
||||
|
||||
const findBest = (qIdx, tIdx, currentGap) => {
|
||||
if (qIdx === n) {
|
||||
return { done: true, positions: [...positions], gap: currentGap };
|
||||
}
|
||||
|
||||
const memoKey = key(qIdx, tIdx, currentGap);
|
||||
if (memo.has(memoKey)) {
|
||||
return memo.get(memoKey);
|
||||
}
|
||||
|
||||
let bestResult = null;
|
||||
|
||||
for (let i = tIdx; i < m; i++) {
|
||||
if (target[i] === query[qIdx]) {
|
||||
positions.push(i);
|
||||
const gap = qIdx === 0 ? 0 : i - positions[positions.length - 2] - 1;
|
||||
const newGap = currentGap + gap;
|
||||
|
||||
if (newGap > m) {
|
||||
positions.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = findBest(qIdx + 1, i + 1, newGap);
|
||||
positions.pop();
|
||||
|
||||
if (result && (!bestResult || result.gap < bestResult.gap)) {
|
||||
bestResult = result;
|
||||
if (result.gap === 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memo.set(memoKey, bestResult);
|
||||
return bestResult;
|
||||
};
|
||||
|
||||
const result = findBest(0, 0, 0);
|
||||
if (!result) return null;
|
||||
|
||||
const consecutive = (() => {
|
||||
let c = 1;
|
||||
for (let i = 1; i < result.positions.length; i++) {
|
||||
if (result.positions[i] === result.positions[i - 1] + 1) {
|
||||
c++;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
})();
|
||||
|
||||
return {
|
||||
positions: result.positions,
|
||||
consecutive,
|
||||
score: calculateMatchScore(query, target, result.positions, consecutive),
|
||||
};
|
||||
};
|
||||
|
||||
const calculateMatchScore = (query, target, positions, consecutive) => {
|
||||
const n = positions.length;
|
||||
const m = target.length;
|
||||
|
||||
if (n === 0) return 0;
|
||||
|
||||
let score = 1.0;
|
||||
|
||||
const startBonus = (m - positions[0]) / m;
|
||||
score += startBonus * 0.5;
|
||||
|
||||
let gapPenalty = 0;
|
||||
for (let i = 1; i < n; i++) {
|
||||
const gap = positions[i] - positions[i - 1] - 1;
|
||||
if (gap > 0) {
|
||||
gapPenalty += Math.min(gap / m, 1.0) * 0.3;
|
||||
}
|
||||
}
|
||||
score -= gapPenalty;
|
||||
|
||||
const consecutiveBonus = consecutive / n;
|
||||
score += consecutiveBonus * 0.3;
|
||||
|
||||
let boundaryBonus = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const char = target[positions[i]];
|
||||
if (i === 0 || isWordBoundary(char)) {
|
||||
boundaryBonus += 0.05;
|
||||
}
|
||||
if (i > 0) {
|
||||
const prevChar = target[positions[i - 1]];
|
||||
if (isCaseTransition(prevChar, char)) {
|
||||
boundaryBonus += 0.03;
|
||||
}
|
||||
}
|
||||
}
|
||||
score = Math.min(1.0, score + boundaryBonus);
|
||||
|
||||
const lengthPenalty = Math.abs(query.length - n) / Math.max(query.length, m);
|
||||
score -= lengthPenalty * 0.2;
|
||||
|
||||
return Math.max(0, Math.min(1.0, score));
|
||||
};
|
||||
|
||||
const fuzzyMatch = (query, target) => {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const lowerTarget = target.toLowerCase();
|
||||
|
||||
if (lowerQuery.length === 0) return null;
|
||||
if (lowerTarget.length === 0) return null;
|
||||
|
||||
if (lowerTarget === lowerQuery) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
if (lowerTarget.includes(lowerQuery)) {
|
||||
const ratio = lowerQuery.length / lowerTarget.length;
|
||||
return 0.8 + ratio * 0.2;
|
||||
}
|
||||
|
||||
const match = findBestSubsequenceMatch(lowerQuery, lowerTarget);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.min(1.0, match.score);
|
||||
};
|
||||
|
||||
self.onmessage = function (e) {
|
||||
const { messageId, type, data } = e.data;
|
||||
|
||||
const respond = (type, data) => {
|
||||
self.postMessage({ messageId, type, data });
|
||||
};
|
||||
|
||||
const respondError = (error) => {
|
||||
self.postMessage({
|
||||
messageId,
|
||||
type: "error",
|
||||
error: error.message || String(error),
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
if (type === "tokenize") {
|
||||
const text = typeof data === "string" ? data : "";
|
||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||
const tokens = words.filter((word) => word.length > 2);
|
||||
const uniqueTokens = Array.from(new Set(tokens));
|
||||
respond("tokens", uniqueTokens);
|
||||
} else if (type === "search") {
|
||||
const { query, limit = 10 } = data;
|
||||
|
||||
if (!query || typeof query !== "string") {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawQuery = query.toLowerCase();
|
||||
const text = typeof query === "string" ? query : "";
|
||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||
const searchTerms = words.filter((word) => word.length > 2);
|
||||
|
||||
let documents = [];
|
||||
if (typeof data.documents === "string") {
|
||||
documents = JSON.parse(data.documents);
|
||||
} else if (Array.isArray(data.documents)) {
|
||||
documents = data.documents;
|
||||
} else if (typeof data.transferables === "string") {
|
||||
documents = JSON.parse(data.transferables);
|
||||
}
|
||||
|
||||
if (!Array.isArray(documents) || documents.length === 0) {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const useFuzzySearch = rawQuery.length >= 3;
|
||||
|
||||
if (searchTerms.length === 0 && rawQuery.length < 3) {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const pageMatches = new Map();
|
||||
|
||||
// Pre-compute lower-case strings for each document
|
||||
const processedDocs = documents.map((doc, docId) => {
|
||||
const title = typeof doc.title === "string" ? doc.title : "";
|
||||
const content = typeof doc.content === "string" ? doc.content : "";
|
||||
|
||||
return {
|
||||
docId,
|
||||
doc,
|
||||
lowerTitle: title.toLowerCase(),
|
||||
lowerContent: content.toLowerCase(),
|
||||
};
|
||||
});
|
||||
|
||||
// First pass: Score pages with fuzzy matching
|
||||
processedDocs.forEach(({ docId, doc, lowerTitle, lowerContent }) => {
|
||||
let match = pageMatches.get(docId);
|
||||
if (!match) {
|
||||
match = { doc, pageScore: 0, matchingAnchors: [] };
|
||||
pageMatches.set(docId, match);
|
||||
}
|
||||
|
||||
if (useFuzzySearch) {
|
||||
const fuzzyTitleScore = fuzzyMatch(rawQuery, lowerTitle);
|
||||
if (fuzzyTitleScore !== null) {
|
||||
match.pageScore += fuzzyTitleScore * 100;
|
||||
}
|
||||
|
||||
const fuzzyContentScore = fuzzyMatch(rawQuery, lowerContent);
|
||||
if (fuzzyContentScore !== null) {
|
||||
match.pageScore += fuzzyContentScore * 30;
|
||||
}
|
||||
}
|
||||
|
||||
// Token-based exact matching
|
||||
searchTerms.forEach((term) => {
|
||||
if (lowerTitle.includes(term)) {
|
||||
match.pageScore += lowerTitle === term ? 20 : 10;
|
||||
}
|
||||
if (lowerContent.includes(term)) {
|
||||
match.pageScore += 2;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Second pass: Find matching anchors
|
||||
pageMatches.forEach((match) => {
|
||||
const doc = match.doc;
|
||||
if (
|
||||
!doc.anchors ||
|
||||
!Array.isArray(doc.anchors) ||
|
||||
doc.anchors.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.anchors.forEach((anchor) => {
|
||||
if (!anchor || !anchor.text) return;
|
||||
|
||||
const anchorText = anchor.text.toLowerCase();
|
||||
let anchorMatches = false;
|
||||
|
||||
if (useFuzzySearch) {
|
||||
const fuzzyScore = fuzzyMatch(rawQuery, anchorText);
|
||||
if (fuzzyScore !== null && fuzzyScore >= 0.4) {
|
||||
anchorMatches = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anchorMatches) {
|
||||
searchTerms.forEach((term) => {
|
||||
if (anchorText.includes(term)) {
|
||||
anchorMatches = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (anchorMatches) {
|
||||
match.matchingAnchors.push(anchor);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const results = Array.from(pageMatches.values())
|
||||
.filter((m) => m.pageScore > 5)
|
||||
.sort((a, b) => b.pageScore - a.pageScore)
|
||||
.slice(0, limit);
|
||||
|
||||
respond("results", results);
|
||||
}
|
||||
} catch (error) {
|
||||
respondError(error);
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -1,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 & 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 & Suggestions</h2>
|
||||
<p>Some quirks are not exactly quirks, but bugs in the module system. If you notice
|
||||
any issues with <strong>nvf</strong>, or this documentation, then please consider reporting
|
||||
them over at the <a href="https://github.com/notashelf/nvf/issues">issue tracker</a>. Issues tab, in addition to the
|
||||
<a href="https://github.com/notashelf/nvf/discussions">discussions tab</a> is a good place as any to request new features.</p>
|
||||
<p>You may also consider submitting bug fixes, feature additions and upstreamed
|
||||
changes that you think are critical over at the <a href="https://github.com/notashelf/nvf/pulls">pull requests tab</a>.</p>
|
||||
</body></html></main>
|
||||
</div>
|
||||
|
||||
<aside class="page-toc">
|
||||
<nav class="page-toc-nav">
|
||||
<h3>On this page</h3>
|
||||
<ul class="page-toc-list">
|
||||
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
|
||||
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
|
||||
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
|
||||
</ul><li><a href="#ch-bugs-suggestions">Bugs & Suggestions</a>
|
||||
</li></ul></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<footer>
|
||||
<p>Generated with ndg</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,110 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NVF - Search</title>
|
||||
|
||||
<script>
|
||||
// Apply sidebar state immediately to prevent flash
|
||||
(function () {
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/style.css" />
|
||||
<script defer src="assets/main.js"></script>
|
||||
<script>
|
||||
window.searchNamespace = window.searchNamespace || {};
|
||||
window.searchNamespace.rootPath = "";
|
||||
</script>
|
||||
<script defer src="assets/search.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-left">
|
||||
<h1 class="site-title">
|
||||
<a href="index.html">NVF</a>
|
||||
</h1>
|
||||
</div>
|
||||
<nav class="header-nav">
|
||||
<ul>
|
||||
<li >
|
||||
<a href="options.html">Options</a>
|
||||
</li>
|
||||
|
||||
<li><a href="search.html">Search</a></li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="search-container">
|
||||
<input type="text" id="search-input" placeholder="Search..." />
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<div class="sidebar-toggle" aria-label="Toggle sidebar">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<nav id="sidebar" class="sidebar">
|
||||
<div class="docs-nav">
|
||||
<h2>Documents</h2>
|
||||
<ul>
|
||||
<li><a href="index.html">Introduction</a></li>
|
||||
<li><a href="configuring.html">Configuring nvf</a></li>
|
||||
<li><a href="hacking.html">Hacking nvf</a></li>
|
||||
<li><a href="tips.html">Helpful Tips</a></li>
|
||||
<li><a href="quirks.html">Known Issues and Quirks</a></li>
|
||||
<li><a href="release-notes.html">Release Notes</a></li>
|
||||
<li><a href="search.html">Search</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="toc">
|
||||
<h2>Contents</h2>
|
||||
<ul class="toc-list">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
<h1>Search</h1>
|
||||
<div class="search-page">
|
||||
<div class="search-form">
|
||||
<input
|
||||
type="text"
|
||||
id="search-page-input"
|
||||
placeholder="Search..."
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<div id="search-page-results" class="search-page-results"></div>
|
||||
</div>
|
||||
<div class="footnotes-container">
|
||||
<!-- Footnotes will be appended here -->
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Generated with ndg</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,740 +0,0 @@
|
|||
// Polyfill for requestIdleCallback for Safari and unsupported browsers
|
||||
if (typeof window.requestIdleCallback === "undefined") {
|
||||
window.requestIdleCallback = function (cb) {
|
||||
const start = Date.now();
|
||||
const idlePeriod = 50;
|
||||
return setTimeout(function () {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
timeRemaining: function () {
|
||||
return Math.max(0, idlePeriod - (Date.now() - start));
|
||||
},
|
||||
});
|
||||
}, 1);
|
||||
};
|
||||
window.cancelIdleCallback = function (id) {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}
|
||||
|
||||
// Create mobile elements if they don't exist
|
||||
function createMobileElements() {
|
||||
// Create mobile sidebar FAB
|
||||
const mobileFab = document.createElement("button");
|
||||
mobileFab.className = "mobile-sidebar-fab";
|
||||
mobileFab.setAttribute("aria-label", "Toggle sidebar menu");
|
||||
mobileFab.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Only show FAB on mobile (max-width: 800px)
|
||||
function updateFabVisibility() {
|
||||
if (window.innerWidth > 800) {
|
||||
if (mobileFab.parentNode) mobileFab.parentNode.removeChild(mobileFab);
|
||||
} else {
|
||||
if (!document.body.contains(mobileFab)) {
|
||||
document.body.appendChild(mobileFab);
|
||||
}
|
||||
mobileFab.style.display = "flex";
|
||||
}
|
||||
}
|
||||
updateFabVisibility();
|
||||
window.addEventListener("resize", updateFabVisibility);
|
||||
|
||||
// Create mobile sidebar container
|
||||
const mobileContainer = document.createElement("div");
|
||||
mobileContainer.className = "mobile-sidebar-container";
|
||||
mobileContainer.innerHTML = `
|
||||
<div class="mobile-sidebar-handle">
|
||||
<div class="mobile-sidebar-dragger"></div>
|
||||
</div>
|
||||
<div class="mobile-sidebar-content">
|
||||
<!-- Sidebar content will be cloned here -->
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create mobile search popup
|
||||
const mobileSearchPopup = document.createElement("div");
|
||||
mobileSearchPopup.id = "mobile-search-popup";
|
||||
mobileSearchPopup.className = "mobile-search-popup";
|
||||
mobileSearchPopup.innerHTML = `
|
||||
<div class="mobile-search-container">
|
||||
<div class="mobile-search-header">
|
||||
<input type="text" id="mobile-search-input" placeholder="Search..." />
|
||||
<button id="close-mobile-search" class="close-mobile-search" aria-label="Close search">×</button>
|
||||
</div>
|
||||
<div id="mobile-search-results" class="mobile-search-results"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert at end of body so it is not affected by .container flex or stacking context
|
||||
document.body.appendChild(mobileContainer);
|
||||
document.body.appendChild(mobileSearchPopup);
|
||||
|
||||
// Immediately populate mobile sidebar content if desktop sidebar exists
|
||||
const desktopSidebar = document.querySelector(".sidebar");
|
||||
const mobileSidebarContent = mobileContainer.querySelector(
|
||||
".mobile-sidebar-content",
|
||||
);
|
||||
if (desktopSidebar && mobileSidebarContent) {
|
||||
mobileSidebarContent.innerHTML = desktopSidebar.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize collapsible sidebar sections with state persistence
|
||||
function initCollapsibleSections() {
|
||||
// Target sections in both desktop and mobile sidebars
|
||||
const sections = document.querySelectorAll(
|
||||
".sidebar .sidebar-section, .mobile-sidebar-content .sidebar-section",
|
||||
);
|
||||
|
||||
sections.forEach((section) => {
|
||||
const sectionId = section.dataset.section;
|
||||
if (!sectionId) return;
|
||||
|
||||
const storageKey = `sidebar-section-${sectionId}`;
|
||||
const savedState = localStorage.getItem(storageKey);
|
||||
|
||||
// Restore saved state (default is open)
|
||||
if (savedState === "closed") {
|
||||
section.removeAttribute("open");
|
||||
}
|
||||
|
||||
// Save state on toggle and sync between desktop/mobile
|
||||
section.addEventListener("toggle", () => {
|
||||
localStorage.setItem(storageKey, section.open ? "open" : "closed");
|
||||
|
||||
// Sync state between desktop and mobile versions
|
||||
const allWithSameSection = document.querySelectorAll(
|
||||
`.sidebar-section[data-section="${sectionId}"]`,
|
||||
);
|
||||
allWithSameSection.forEach((el) => {
|
||||
if (el !== section) {
|
||||
el.open = section.open;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize scroll spy
|
||||
function initScrollSpy() {
|
||||
const pageToc = document.querySelector(".page-toc");
|
||||
if (!pageToc) return;
|
||||
|
||||
const tocLinks = pageToc.querySelectorAll(".page-toc-list a");
|
||||
const content = document.querySelector(".content");
|
||||
if (!tocLinks.length || !content) return;
|
||||
|
||||
const headings = Array.from(
|
||||
content.querySelectorAll("h1[id], h2[id], h3[id]"),
|
||||
);
|
||||
|
||||
if (!headings.length) return;
|
||||
|
||||
// Build a map of heading IDs to TOC links for quick lookup
|
||||
const linkMap = new Map();
|
||||
tocLinks.forEach((link) => {
|
||||
const href = link.getAttribute("href");
|
||||
if (href && href.startsWith("#")) {
|
||||
linkMap.set(href.slice(1), link);
|
||||
}
|
||||
});
|
||||
|
||||
let activeLink = null;
|
||||
|
||||
// Update active link based on scroll position
|
||||
function updateActiveLink() {
|
||||
const threshold = 120; // threshold from the top of the viewport
|
||||
|
||||
let currentHeading = null;
|
||||
|
||||
// Find the last heading that is at or above the threshold
|
||||
for (const heading of headings) {
|
||||
const rect = heading.getBoundingClientRect();
|
||||
if (rect.top <= threshold) {
|
||||
currentHeading = heading;
|
||||
}
|
||||
}
|
||||
|
||||
// If no heading is above threshold, use first heading if it's in view
|
||||
if (!currentHeading && headings.length > 0) {
|
||||
const firstRect = headings[0].getBoundingClientRect();
|
||||
if (firstRect.top < window.innerHeight) {
|
||||
currentHeading = headings[0];
|
||||
}
|
||||
}
|
||||
|
||||
const newLink = currentHeading ? linkMap.get(currentHeading.id) : null;
|
||||
|
||||
if (newLink !== activeLink) {
|
||||
if (activeLink) {
|
||||
activeLink.classList.remove("active");
|
||||
}
|
||||
if (newLink) {
|
||||
newLink.classList.add("active");
|
||||
}
|
||||
activeLink = newLink;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll event handler
|
||||
let ticking = false;
|
||||
function onScroll() {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
updateActiveLink();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
// Also update on hash change (direct link navigation)
|
||||
window.addEventListener("hashchange", () => {
|
||||
requestAnimationFrame(updateActiveLink);
|
||||
});
|
||||
|
||||
// Set initial active state after a small delay to ensure
|
||||
// browser has completed any hash-based scrolling
|
||||
setTimeout(updateActiveLink, 100);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Apply sidebar state immediately before DOM rendering
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
document.body.classList.add("sidebar-collapsed");
|
||||
}
|
||||
|
||||
if (!document.querySelector(".mobile-sidebar-fab")) {
|
||||
createMobileElements();
|
||||
}
|
||||
|
||||
// Initialize collapsible sidebar sections
|
||||
// after mobile elements are created
|
||||
initCollapsibleSections();
|
||||
|
||||
// Initialize scroll spy for page TOC
|
||||
initScrollSpy();
|
||||
|
||||
// Desktop Sidebar Toggle
|
||||
const sidebarToggle = document.querySelector(".sidebar-toggle");
|
||||
|
||||
// On page load, sync the state from `documentElement` to `body`
|
||||
if (document.documentElement.classList.contains("sidebar-collapsed")) {
|
||||
document.body.classList.add("sidebar-collapsed");
|
||||
}
|
||||
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener("click", function () {
|
||||
// Toggle on both elements for consistency
|
||||
document.documentElement.classList.toggle("sidebar-collapsed");
|
||||
document.body.classList.toggle("sidebar-collapsed");
|
||||
|
||||
// Use documentElement to check state and save to localStorage
|
||||
const isCollapsed = document.documentElement.classList.contains(
|
||||
"sidebar-collapsed",
|
||||
);
|
||||
localStorage.setItem("sidebar-collapsed", isCollapsed);
|
||||
});
|
||||
}
|
||||
|
||||
// Make headings clickable for anchor links
|
||||
const content = document.querySelector(".content");
|
||||
if (content) {
|
||||
const headings = content.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
||||
|
||||
headings.forEach(function (heading) {
|
||||
// Generate a valid, unique ID for each heading
|
||||
if (!heading.id) {
|
||||
let baseId = heading.textContent
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-_]/g, "") // remove invalid chars
|
||||
.replace(/^[^a-z]+/, "") // remove leading non-letters
|
||||
.replace(/[\s-_]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") // trim leading/trailing dashes
|
||||
.trim();
|
||||
if (!baseId) {
|
||||
baseId = "section";
|
||||
}
|
||||
let id = baseId;
|
||||
let counter = 1;
|
||||
while (document.getElementById(id)) {
|
||||
id = `${baseId}-${counter++}`;
|
||||
}
|
||||
heading.id = id;
|
||||
}
|
||||
|
||||
// Make the entire heading clickable
|
||||
heading.addEventListener("click", function () {
|
||||
const id = this.id;
|
||||
history.pushState(null, null, "#" + id);
|
||||
|
||||
// Scroll with offset
|
||||
const offset = this.getBoundingClientRect().top + window.scrollY - 80;
|
||||
window.scrollTo({
|
||||
top: offset,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Process footnotes
|
||||
if (content) {
|
||||
const footnoteContainer = document.querySelector(".footnotes-container");
|
||||
|
||||
// Find all footnote references and create a footnotes section
|
||||
const footnoteRefs = content.querySelectorAll('a[href^="#fn"]');
|
||||
if (footnoteRefs.length > 0) {
|
||||
const footnotesDiv = document.createElement("div");
|
||||
footnotesDiv.className = "footnotes";
|
||||
|
||||
const footnotesHeading = document.createElement("h2");
|
||||
footnotesHeading.textContent = "Footnotes";
|
||||
footnotesDiv.appendChild(footnotesHeading);
|
||||
|
||||
const footnotesList = document.createElement("ol");
|
||||
footnoteContainer.appendChild(footnotesDiv);
|
||||
footnotesDiv.appendChild(footnotesList);
|
||||
|
||||
// Add footnotes
|
||||
document.querySelectorAll(".footnote").forEach((footnote) => {
|
||||
const id = footnote.id;
|
||||
const content = footnote.innerHTML;
|
||||
|
||||
const li = document.createElement("li");
|
||||
li.id = id;
|
||||
li.innerHTML = content;
|
||||
|
||||
// Add backlink
|
||||
const backlink = document.createElement("a");
|
||||
backlink.href = "#fnref:" + id.replace("fn:", "");
|
||||
backlink.className = "footnote-backlink";
|
||||
backlink.textContent = "↩";
|
||||
li.appendChild(backlink);
|
||||
|
||||
footnotesList.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Copy link functionality
|
||||
document.querySelectorAll(".copy-link").forEach(function (copyLink) {
|
||||
copyLink.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Get option ID from parent element
|
||||
const option = copyLink.closest(".option");
|
||||
const optionId = option.id;
|
||||
|
||||
// Create URL with hash
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = optionId;
|
||||
|
||||
// Copy to clipboard
|
||||
navigator.clipboard
|
||||
.writeText(url.toString())
|
||||
.then(function () {
|
||||
// Show feedback
|
||||
const feedback = copyLink.nextElementSibling;
|
||||
feedback.style.display = "inline";
|
||||
|
||||
// Hide after 2 seconds
|
||||
setTimeout(function () {
|
||||
feedback.style.display = "none";
|
||||
}, 2000);
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("Could not copy link: ", err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Handle initial hash navigation
|
||||
function scrollToElement(element) {
|
||||
if (element) {
|
||||
const offset = element.getBoundingClientRect().top + window.scrollY - 80;
|
||||
window.scrollTo({
|
||||
top: offset,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (window.location.hash) {
|
||||
const targetElement = document.querySelector(window.location.hash);
|
||||
if (targetElement) {
|
||||
setTimeout(() => scrollToElement(targetElement), 0);
|
||||
// Add highlight class for options page
|
||||
if (targetElement.classList.contains("option")) {
|
||||
targetElement.classList.add("highlight");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile Sidebar Functionality
|
||||
const mobileSidebarContainer = document.querySelector(
|
||||
".mobile-sidebar-container",
|
||||
);
|
||||
const mobileSidebarFab = document.querySelector(".mobile-sidebar-fab");
|
||||
const mobileSidebarHandle = document.querySelector(".mobile-sidebar-handle");
|
||||
|
||||
// Always set up FAB if it exists
|
||||
if (mobileSidebarFab && mobileSidebarContainer) {
|
||||
const openMobileSidebar = () => {
|
||||
mobileSidebarContainer.classList.add("active");
|
||||
mobileSidebarFab.setAttribute("aria-expanded", "true");
|
||||
mobileSidebarContainer.setAttribute("aria-hidden", "false");
|
||||
mobileSidebarFab.classList.add("fab-hidden"); // hide FAB when drawer is open
|
||||
};
|
||||
|
||||
const closeMobileSidebar = () => {
|
||||
mobileSidebarContainer.classList.remove("active");
|
||||
mobileSidebarFab.setAttribute("aria-expanded", "false");
|
||||
mobileSidebarContainer.setAttribute("aria-hidden", "true");
|
||||
mobileSidebarFab.classList.remove("fab-hidden"); // Show FAB when drawer is closed
|
||||
};
|
||||
|
||||
mobileSidebarFab.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (mobileSidebarContainer.classList.contains("active")) {
|
||||
closeMobileSidebar();
|
||||
} else {
|
||||
openMobileSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Only set up drag functionality if handle exists
|
||||
if (mobileSidebarHandle) {
|
||||
// Drag functionality
|
||||
let isDragging = false;
|
||||
let startY = 0;
|
||||
let startHeight = 0;
|
||||
|
||||
// Cleanup function for drag interruption
|
||||
function cleanupDrag() {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
mobileSidebarHandle.style.cursor = "grab";
|
||||
document.body.style.userSelect = "";
|
||||
}
|
||||
}
|
||||
|
||||
mobileSidebarHandle.addEventListener("mousedown", (e) => {
|
||||
isDragging = true;
|
||||
startY = e.pageY;
|
||||
startHeight = mobileSidebarContainer.offsetHeight;
|
||||
mobileSidebarHandle.style.cursor = "grabbing";
|
||||
document.body.style.userSelect = "none"; // prevent text selection
|
||||
});
|
||||
|
||||
mobileSidebarHandle.addEventListener("touchstart", (e) => {
|
||||
isDragging = true;
|
||||
startY = e.touches[0].pageY;
|
||||
startHeight = mobileSidebarContainer.offsetHeight;
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
if (!isDragging) return;
|
||||
const deltaY = startY - e.pageY;
|
||||
const newHeight = startHeight + deltaY;
|
||||
const vh = window.innerHeight;
|
||||
const minHeight = vh * 0.15;
|
||||
const maxHeight = vh * 0.9;
|
||||
|
||||
if (newHeight >= minHeight && newHeight <= maxHeight) {
|
||||
mobileSidebarContainer.style.height = `${newHeight}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("touchmove", (e) => {
|
||||
if (!isDragging) return;
|
||||
const deltaY = startY - e.touches[0].pageY;
|
||||
const newHeight = startHeight + deltaY;
|
||||
const vh = window.innerHeight;
|
||||
const minHeight = vh * 0.15;
|
||||
const maxHeight = vh * 0.9;
|
||||
|
||||
if (newHeight >= minHeight && newHeight <= maxHeight) {
|
||||
mobileSidebarContainer.style.height = `${newHeight}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mouseup", cleanupDrag);
|
||||
document.addEventListener("touchend", cleanupDrag);
|
||||
window.addEventListener("blur", cleanupDrag);
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (document.hidden) cleanupDrag();
|
||||
});
|
||||
}
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener("click", (event) => {
|
||||
if (
|
||||
mobileSidebarContainer.classList.contains("active") &&
|
||||
!mobileSidebarContainer.contains(event.target) &&
|
||||
!mobileSidebarFab.contains(event.target)
|
||||
) {
|
||||
closeMobileSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on escape key
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (
|
||||
event.key === "Escape" &&
|
||||
mobileSidebarContainer.classList.contains("active")
|
||||
) {
|
||||
closeMobileSidebar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Options filter functionality
|
||||
const optionsFilter = document.getElementById("options-filter");
|
||||
if (optionsFilter) {
|
||||
const optionsContainer = document.querySelector(".options-container");
|
||||
if (!optionsContainer) return;
|
||||
|
||||
// Only inject the style if it doesn't already exist
|
||||
if (!document.head.querySelector("style[data-options-hidden]")) {
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.setAttribute("data-options-hidden", "");
|
||||
styleEl.textContent = ".option-hidden{display:none!important}";
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
// Create filter results counter
|
||||
const filterResults = document.createElement("div");
|
||||
filterResults.className = "filter-results";
|
||||
optionsFilter.parentNode.insertBefore(
|
||||
filterResults,
|
||||
optionsFilter.nextSibling,
|
||||
);
|
||||
|
||||
// Detect if we're on a mobile device
|
||||
const isMobile = window.innerWidth < 768 ||
|
||||
/Mobi|Android/i.test(navigator.userAgent);
|
||||
|
||||
// Cache all option elements and their searchable content
|
||||
const options = Array.from(document.querySelectorAll(".option"));
|
||||
const totalCount = options.length;
|
||||
|
||||
// Store the original order of option elements
|
||||
const originalOptionOrder = options.slice();
|
||||
|
||||
// Pre-process and optimize searchable content
|
||||
const optionsData = options.map((option) => {
|
||||
const nameElem = option.querySelector(".option-name");
|
||||
const descriptionElem = option.querySelector(".option-description");
|
||||
const id = option.id ? option.id.toLowerCase() : "";
|
||||
const name = nameElem ? nameElem.textContent.toLowerCase() : "";
|
||||
const description = descriptionElem
|
||||
? descriptionElem.textContent.toLowerCase()
|
||||
: "";
|
||||
|
||||
// Extract keywords for faster searching
|
||||
const keywords = (id + " " + name + " " + description)
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((word) => word.length > 1);
|
||||
|
||||
return {
|
||||
element: option,
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
keywords,
|
||||
searchText: (id + " " + name + " " + description).toLowerCase(),
|
||||
};
|
||||
});
|
||||
|
||||
// Chunk size and rendering variables
|
||||
const CHUNK_SIZE = isMobile ? 15 : 40;
|
||||
let pendingRender = null;
|
||||
let currentChunk = 0;
|
||||
let itemsToProcess = [];
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Process options in chunks to prevent UI freezing
|
||||
function processNextChunk() {
|
||||
const startIdx = currentChunk * CHUNK_SIZE;
|
||||
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
|
||||
|
||||
if (startIdx < itemsToProcess.length) {
|
||||
// Process current chunk
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
const item = itemsToProcess[i];
|
||||
if (item.visible) {
|
||||
item.element.classList.remove("option-hidden");
|
||||
} else {
|
||||
item.element.classList.add("option-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
currentChunk++;
|
||||
pendingRender = requestAnimationFrame(processNextChunk);
|
||||
} else {
|
||||
// Finished processing all chunks
|
||||
pendingRender = null;
|
||||
currentChunk = 0;
|
||||
itemsToProcess = [];
|
||||
|
||||
// Update counter at the very end for best performance
|
||||
if (filterResults.visibleCount !== undefined) {
|
||||
if (filterResults.visibleCount < totalCount) {
|
||||
filterResults.textContent =
|
||||
`Showing ${filterResults.visibleCount} of ${totalCount} options`;
|
||||
filterResults.style.display = "block";
|
||||
} else {
|
||||
filterResults.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterOptions() {
|
||||
const searchTerm = optionsFilter.value.toLowerCase().trim();
|
||||
|
||||
if (pendingRender) {
|
||||
cancelAnimationFrame(pendingRender);
|
||||
pendingRender = null;
|
||||
}
|
||||
currentChunk = 0;
|
||||
itemsToProcess = [];
|
||||
|
||||
if (searchTerm === "") {
|
||||
// Restore original DOM order when filter is cleared
|
||||
const fragment = document.createDocumentFragment();
|
||||
originalOptionOrder.forEach((option) => {
|
||||
option.classList.remove("option-hidden");
|
||||
fragment.appendChild(option);
|
||||
});
|
||||
optionsContainer.appendChild(fragment);
|
||||
filterResults.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const searchTerms = searchTerm
|
||||
.split(/\s+/)
|
||||
.filter((term) => term.length > 0);
|
||||
let visibleCount = 0;
|
||||
|
||||
const titleMatches = [];
|
||||
const descMatches = [];
|
||||
optionsData.forEach((data) => {
|
||||
let isTitleMatch = false;
|
||||
let isDescMatch = false;
|
||||
if (searchTerms.length === 1) {
|
||||
const term = searchTerms[0];
|
||||
isTitleMatch = data.name.includes(term);
|
||||
isDescMatch = !isTitleMatch && data.description.includes(term);
|
||||
} else {
|
||||
isTitleMatch = searchTerms.every((term) => data.name.includes(term));
|
||||
isDescMatch = !isTitleMatch &&
|
||||
searchTerms.every((term) => data.description.includes(term));
|
||||
}
|
||||
if (isTitleMatch) {
|
||||
titleMatches.push(data);
|
||||
} else if (isDescMatch) {
|
||||
descMatches.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
if (searchTerms.length === 1) {
|
||||
const term = searchTerms[0];
|
||||
titleMatches.sort(
|
||||
(a, b) => a.name.indexOf(term) - b.name.indexOf(term),
|
||||
);
|
||||
descMatches.sort(
|
||||
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
|
||||
);
|
||||
}
|
||||
|
||||
itemsToProcess = [];
|
||||
titleMatches.forEach((data) => {
|
||||
visibleCount++;
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
});
|
||||
descMatches.forEach((data) => {
|
||||
visibleCount++;
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
});
|
||||
optionsData.forEach((data) => {
|
||||
if (!itemsToProcess.some((item) => item.element === data.element)) {
|
||||
itemsToProcess.push({ element: data.element, visible: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Reorder DOM so all title matches, then desc matches, then hidden
|
||||
const fragment = document.createDocumentFragment();
|
||||
itemsToProcess.forEach((item) => {
|
||||
fragment.appendChild(item.element);
|
||||
});
|
||||
optionsContainer.appendChild(fragment);
|
||||
|
||||
filterResults.visibleCount = visibleCount;
|
||||
pendingRender = requestAnimationFrame(processNextChunk);
|
||||
}
|
||||
|
||||
// Use different debounce times for desktop vs mobile
|
||||
const debouncedFilter = debounce(filterOptions, isMobile ? 200 : 100);
|
||||
|
||||
// Set up event listeners
|
||||
optionsFilter.addEventListener("input", debouncedFilter);
|
||||
optionsFilter.addEventListener("change", filterOptions);
|
||||
|
||||
// Allow clearing with Escape key
|
||||
optionsFilter.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") {
|
||||
optionsFilter.value = "";
|
||||
filterOptions();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle visibility changes
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (!document.hidden && optionsFilter.value) {
|
||||
filterOptions();
|
||||
}
|
||||
});
|
||||
|
||||
// Initially trigger filter if there's a value
|
||||
if (optionsFilter.value) {
|
||||
filterOptions();
|
||||
}
|
||||
|
||||
// Pre-calculate heights for smoother scrolling
|
||||
if (isMobile && totalCount > 50) {
|
||||
requestIdleCallback(() => {
|
||||
const sampleOption = options[0];
|
||||
if (sampleOption) {
|
||||
const height = sampleOption.offsetHeight;
|
||||
if (height > 0) {
|
||||
options.forEach((opt) => {
|
||||
opt.style.containIntrinsicSize = `0 ${height}px`;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,298 +0,0 @@
|
|||
const isWordBoundary = (char) =>
|
||||
/[A-Z]/.test(char) || /[-_\/.]/.test(char) || /\s/.test(char);
|
||||
|
||||
const isCaseTransition = (prev, curr) => {
|
||||
const prevIsUpper = prev.toLowerCase() !== prev;
|
||||
const currIsUpper = curr.toLowerCase() !== curr;
|
||||
return (
|
||||
prevIsUpper && currIsUpper && prev.toLowerCase() !== curr.toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
const findBestSubsequenceMatch = (query, target) => {
|
||||
const n = query.length;
|
||||
const m = target.length;
|
||||
|
||||
if (n === 0 || m === 0) return null;
|
||||
|
||||
const positions = [];
|
||||
|
||||
const memo = new Map();
|
||||
const key = (qIdx, tIdx, gap) => `${qIdx}:${tIdx}:${gap}`;
|
||||
|
||||
const findBest = (qIdx, tIdx, currentGap) => {
|
||||
if (qIdx === n) {
|
||||
return { done: true, positions: [...positions], gap: currentGap };
|
||||
}
|
||||
|
||||
const memoKey = key(qIdx, tIdx, currentGap);
|
||||
if (memo.has(memoKey)) {
|
||||
return memo.get(memoKey);
|
||||
}
|
||||
|
||||
let bestResult = null;
|
||||
|
||||
for (let i = tIdx; i < m; i++) {
|
||||
if (target[i] === query[qIdx]) {
|
||||
positions.push(i);
|
||||
const gap = qIdx === 0 ? 0 : i - positions[positions.length - 2] - 1;
|
||||
const newGap = currentGap + gap;
|
||||
|
||||
if (newGap > m) {
|
||||
positions.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = findBest(qIdx + 1, i + 1, newGap);
|
||||
positions.pop();
|
||||
|
||||
if (result && (!bestResult || result.gap < bestResult.gap)) {
|
||||
bestResult = result;
|
||||
if (result.gap === 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memo.set(memoKey, bestResult);
|
||||
return bestResult;
|
||||
};
|
||||
|
||||
const result = findBest(0, 0, 0);
|
||||
if (!result) return null;
|
||||
|
||||
const consecutive = (() => {
|
||||
let c = 1;
|
||||
for (let i = 1; i < result.positions.length; i++) {
|
||||
if (result.positions[i] === result.positions[i - 1] + 1) {
|
||||
c++;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
})();
|
||||
|
||||
return {
|
||||
positions: result.positions,
|
||||
consecutive,
|
||||
score: calculateMatchScore(query, target, result.positions, consecutive),
|
||||
};
|
||||
};
|
||||
|
||||
const calculateMatchScore = (query, target, positions, consecutive) => {
|
||||
const n = positions.length;
|
||||
const m = target.length;
|
||||
|
||||
if (n === 0) return 0;
|
||||
|
||||
let score = 1.0;
|
||||
|
||||
const startBonus = (m - positions[0]) / m;
|
||||
score += startBonus * 0.5;
|
||||
|
||||
let gapPenalty = 0;
|
||||
for (let i = 1; i < n; i++) {
|
||||
const gap = positions[i] - positions[i - 1] - 1;
|
||||
if (gap > 0) {
|
||||
gapPenalty += Math.min(gap / m, 1.0) * 0.3;
|
||||
}
|
||||
}
|
||||
score -= gapPenalty;
|
||||
|
||||
const consecutiveBonus = consecutive / n;
|
||||
score += consecutiveBonus * 0.3;
|
||||
|
||||
let boundaryBonus = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const char = target[positions[i]];
|
||||
if (i === 0 || isWordBoundary(char)) {
|
||||
boundaryBonus += 0.05;
|
||||
}
|
||||
if (i > 0) {
|
||||
const prevChar = target[positions[i - 1]];
|
||||
if (isCaseTransition(prevChar, char)) {
|
||||
boundaryBonus += 0.03;
|
||||
}
|
||||
}
|
||||
}
|
||||
score = Math.min(1.0, score + boundaryBonus);
|
||||
|
||||
const lengthPenalty = Math.abs(query.length - n) / Math.max(query.length, m);
|
||||
score -= lengthPenalty * 0.2;
|
||||
|
||||
return Math.max(0, Math.min(1.0, score));
|
||||
};
|
||||
|
||||
const fuzzyMatch = (query, target) => {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const lowerTarget = target.toLowerCase();
|
||||
|
||||
if (lowerQuery.length === 0) return null;
|
||||
if (lowerTarget.length === 0) return null;
|
||||
|
||||
if (lowerTarget === lowerQuery) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
if (lowerTarget.includes(lowerQuery)) {
|
||||
const ratio = lowerQuery.length / lowerTarget.length;
|
||||
return 0.8 + ratio * 0.2;
|
||||
}
|
||||
|
||||
const match = findBestSubsequenceMatch(lowerQuery, lowerTarget);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.min(1.0, match.score);
|
||||
};
|
||||
|
||||
self.onmessage = function (e) {
|
||||
const { messageId, type, data } = e.data;
|
||||
|
||||
const respond = (type, data) => {
|
||||
self.postMessage({ messageId, type, data });
|
||||
};
|
||||
|
||||
const respondError = (error) => {
|
||||
self.postMessage({
|
||||
messageId,
|
||||
type: "error",
|
||||
error: error.message || String(error),
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
if (type === "tokenize") {
|
||||
const text = typeof data === "string" ? data : "";
|
||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||
const tokens = words.filter((word) => word.length > 2);
|
||||
const uniqueTokens = Array.from(new Set(tokens));
|
||||
respond("tokens", uniqueTokens);
|
||||
} else if (type === "search") {
|
||||
const { query, limit = 10 } = data;
|
||||
|
||||
if (!query || typeof query !== "string") {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawQuery = query.toLowerCase();
|
||||
const text = typeof query === "string" ? query : "";
|
||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||
const searchTerms = words.filter((word) => word.length > 2);
|
||||
|
||||
let documents = [];
|
||||
if (typeof data.documents === "string") {
|
||||
documents = JSON.parse(data.documents);
|
||||
} else if (Array.isArray(data.documents)) {
|
||||
documents = data.documents;
|
||||
} else if (typeof data.transferables === "string") {
|
||||
documents = JSON.parse(data.transferables);
|
||||
}
|
||||
|
||||
if (!Array.isArray(documents) || documents.length === 0) {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const useFuzzySearch = rawQuery.length >= 3;
|
||||
|
||||
if (searchTerms.length === 0 && rawQuery.length < 3) {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const pageMatches = new Map();
|
||||
|
||||
// Pre-compute lower-case strings for each document
|
||||
const processedDocs = documents.map((doc, docId) => {
|
||||
const title = typeof doc.title === "string" ? doc.title : "";
|
||||
const content = typeof doc.content === "string" ? doc.content : "";
|
||||
|
||||
return {
|
||||
docId,
|
||||
doc,
|
||||
lowerTitle: title.toLowerCase(),
|
||||
lowerContent: content.toLowerCase(),
|
||||
};
|
||||
});
|
||||
|
||||
// First pass: Score pages with fuzzy matching
|
||||
processedDocs.forEach(({ docId, doc, lowerTitle, lowerContent }) => {
|
||||
let match = pageMatches.get(docId);
|
||||
if (!match) {
|
||||
match = { doc, pageScore: 0, matchingAnchors: [] };
|
||||
pageMatches.set(docId, match);
|
||||
}
|
||||
|
||||
if (useFuzzySearch) {
|
||||
const fuzzyTitleScore = fuzzyMatch(rawQuery, lowerTitle);
|
||||
if (fuzzyTitleScore !== null) {
|
||||
match.pageScore += fuzzyTitleScore * 100;
|
||||
}
|
||||
|
||||
const fuzzyContentScore = fuzzyMatch(rawQuery, lowerContent);
|
||||
if (fuzzyContentScore !== null) {
|
||||
match.pageScore += fuzzyContentScore * 30;
|
||||
}
|
||||
}
|
||||
|
||||
// Token-based exact matching
|
||||
searchTerms.forEach((term) => {
|
||||
if (lowerTitle.includes(term)) {
|
||||
match.pageScore += lowerTitle === term ? 20 : 10;
|
||||
}
|
||||
if (lowerContent.includes(term)) {
|
||||
match.pageScore += 2;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Second pass: Find matching anchors
|
||||
pageMatches.forEach((match) => {
|
||||
const doc = match.doc;
|
||||
if (
|
||||
!doc.anchors ||
|
||||
!Array.isArray(doc.anchors) ||
|
||||
doc.anchors.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.anchors.forEach((anchor) => {
|
||||
if (!anchor || !anchor.text) return;
|
||||
|
||||
const anchorText = anchor.text.toLowerCase();
|
||||
let anchorMatches = false;
|
||||
|
||||
if (useFuzzySearch) {
|
||||
const fuzzyScore = fuzzyMatch(rawQuery, anchorText);
|
||||
if (fuzzyScore !== null && fuzzyScore >= 0.4) {
|
||||
anchorMatches = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anchorMatches) {
|
||||
searchTerms.forEach((term) => {
|
||||
if (anchorText.includes(term)) {
|
||||
anchorMatches = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (anchorMatches) {
|
||||
match.matchingAnchors.push(anchor);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const results = Array.from(pageMatches.values())
|
||||
.filter((m) => m.pageScore > 5)
|
||||
.sort((a, b) => b.pageScore - a.pageScore)
|
||||
.slice(0, limit);
|
||||
|
||||
respond("results", results);
|
||||
}
|
||||
} catch (error) {
|
||||
respondError(error);
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -1,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 & 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 & Suggestions</h2>
|
||||
<p>Some quirks are not exactly quirks, but bugs in the module system. If you notice
|
||||
any issues with <strong>nvf</strong>, or this documentation, then please consider reporting
|
||||
them over at the <a href="https://github.com/notashelf/nvf/issues">issue tracker</a>. Issues tab, in addition to the
|
||||
<a href="https://github.com/notashelf/nvf/discussions">discussions tab</a> is a good place as any to request new features.</p>
|
||||
<p>You may also consider submitting bug fixes, feature additions and upstreamed
|
||||
changes that you think are critical over at the <a href="https://github.com/notashelf/nvf/pulls">pull requests tab</a>.</p>
|
||||
</body></html></main>
|
||||
</div>
|
||||
|
||||
<aside class="page-toc">
|
||||
<nav class="page-toc-nav">
|
||||
<h3>On this page</h3>
|
||||
<ul class="page-toc-list">
|
||||
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
|
||||
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
|
||||
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
|
||||
</ul><li><a href="#ch-bugs-suggestions">Bugs & Suggestions</a>
|
||||
</li></ul></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<footer>
|
||||
<p>Generated with ndg</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,110 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NVF - Search</title>
|
||||
|
||||
<script>
|
||||
// Apply sidebar state immediately to prevent flash
|
||||
(function () {
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/style.css" />
|
||||
<script defer src="assets/main.js"></script>
|
||||
<script>
|
||||
window.searchNamespace = window.searchNamespace || {};
|
||||
window.searchNamespace.rootPath = "";
|
||||
</script>
|
||||
<script defer src="assets/search.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-left">
|
||||
<h1 class="site-title">
|
||||
<a href="index.html">NVF</a>
|
||||
</h1>
|
||||
</div>
|
||||
<nav class="header-nav">
|
||||
<ul>
|
||||
<li >
|
||||
<a href="options.html">Options</a>
|
||||
</li>
|
||||
|
||||
<li><a href="search.html">Search</a></li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="search-container">
|
||||
<input type="text" id="search-input" placeholder="Search..." />
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<div class="sidebar-toggle" aria-label="Toggle sidebar">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<nav id="sidebar" class="sidebar">
|
||||
<div class="docs-nav">
|
||||
<h2>Documents</h2>
|
||||
<ul>
|
||||
<li><a href="index.html">Introduction</a></li>
|
||||
<li><a href="configuring.html">Configuring nvf</a></li>
|
||||
<li><a href="hacking.html">Hacking nvf</a></li>
|
||||
<li><a href="tips.html">Helpful Tips</a></li>
|
||||
<li><a href="quirks.html">Known Issues and Quirks</a></li>
|
||||
<li><a href="release-notes.html">Release Notes</a></li>
|
||||
<li><a href="search.html">Search</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="toc">
|
||||
<h2>Contents</h2>
|
||||
<ul class="toc-list">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
<h1>Search</h1>
|
||||
<div class="search-page">
|
||||
<div class="search-form">
|
||||
<input
|
||||
type="text"
|
||||
id="search-page-input"
|
||||
placeholder="Search..."
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<div id="search-page-results" class="search-page-results"></div>
|
||||
</div>
|
||||
<div class="footnotes-container">
|
||||
<!-- Footnotes will be appended here -->
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Generated with ndg</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,740 +0,0 @@
|
|||
// Polyfill for requestIdleCallback for Safari and unsupported browsers
|
||||
if (typeof window.requestIdleCallback === "undefined") {
|
||||
window.requestIdleCallback = function (cb) {
|
||||
const start = Date.now();
|
||||
const idlePeriod = 50;
|
||||
return setTimeout(function () {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
timeRemaining: function () {
|
||||
return Math.max(0, idlePeriod - (Date.now() - start));
|
||||
},
|
||||
});
|
||||
}, 1);
|
||||
};
|
||||
window.cancelIdleCallback = function (id) {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}
|
||||
|
||||
// Create mobile elements if they don't exist
|
||||
function createMobileElements() {
|
||||
// Create mobile sidebar FAB
|
||||
const mobileFab = document.createElement("button");
|
||||
mobileFab.className = "mobile-sidebar-fab";
|
||||
mobileFab.setAttribute("aria-label", "Toggle sidebar menu");
|
||||
mobileFab.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Only show FAB on mobile (max-width: 800px)
|
||||
function updateFabVisibility() {
|
||||
if (window.innerWidth > 800) {
|
||||
if (mobileFab.parentNode) mobileFab.parentNode.removeChild(mobileFab);
|
||||
} else {
|
||||
if (!document.body.contains(mobileFab)) {
|
||||
document.body.appendChild(mobileFab);
|
||||
}
|
||||
mobileFab.style.display = "flex";
|
||||
}
|
||||
}
|
||||
updateFabVisibility();
|
||||
window.addEventListener("resize", updateFabVisibility);
|
||||
|
||||
// Create mobile sidebar container
|
||||
const mobileContainer = document.createElement("div");
|
||||
mobileContainer.className = "mobile-sidebar-container";
|
||||
mobileContainer.innerHTML = `
|
||||
<div class="mobile-sidebar-handle">
|
||||
<div class="mobile-sidebar-dragger"></div>
|
||||
</div>
|
||||
<div class="mobile-sidebar-content">
|
||||
<!-- Sidebar content will be cloned here -->
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create mobile search popup
|
||||
const mobileSearchPopup = document.createElement("div");
|
||||
mobileSearchPopup.id = "mobile-search-popup";
|
||||
mobileSearchPopup.className = "mobile-search-popup";
|
||||
mobileSearchPopup.innerHTML = `
|
||||
<div class="mobile-search-container">
|
||||
<div class="mobile-search-header">
|
||||
<input type="text" id="mobile-search-input" placeholder="Search..." />
|
||||
<button id="close-mobile-search" class="close-mobile-search" aria-label="Close search">×</button>
|
||||
</div>
|
||||
<div id="mobile-search-results" class="mobile-search-results"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert at end of body so it is not affected by .container flex or stacking context
|
||||
document.body.appendChild(mobileContainer);
|
||||
document.body.appendChild(mobileSearchPopup);
|
||||
|
||||
// Immediately populate mobile sidebar content if desktop sidebar exists
|
||||
const desktopSidebar = document.querySelector(".sidebar");
|
||||
const mobileSidebarContent = mobileContainer.querySelector(
|
||||
".mobile-sidebar-content",
|
||||
);
|
||||
if (desktopSidebar && mobileSidebarContent) {
|
||||
mobileSidebarContent.innerHTML = desktopSidebar.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize collapsible sidebar sections with state persistence
|
||||
function initCollapsibleSections() {
|
||||
// Target sections in both desktop and mobile sidebars
|
||||
const sections = document.querySelectorAll(
|
||||
".sidebar .sidebar-section, .mobile-sidebar-content .sidebar-section",
|
||||
);
|
||||
|
||||
sections.forEach((section) => {
|
||||
const sectionId = section.dataset.section;
|
||||
if (!sectionId) return;
|
||||
|
||||
const storageKey = `sidebar-section-${sectionId}`;
|
||||
const savedState = localStorage.getItem(storageKey);
|
||||
|
||||
// Restore saved state (default is open)
|
||||
if (savedState === "closed") {
|
||||
section.removeAttribute("open");
|
||||
}
|
||||
|
||||
// Save state on toggle and sync between desktop/mobile
|
||||
section.addEventListener("toggle", () => {
|
||||
localStorage.setItem(storageKey, section.open ? "open" : "closed");
|
||||
|
||||
// Sync state between desktop and mobile versions
|
||||
const allWithSameSection = document.querySelectorAll(
|
||||
`.sidebar-section[data-section="${sectionId}"]`,
|
||||
);
|
||||
allWithSameSection.forEach((el) => {
|
||||
if (el !== section) {
|
||||
el.open = section.open;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize scroll spy
|
||||
function initScrollSpy() {
|
||||
const pageToc = document.querySelector(".page-toc");
|
||||
if (!pageToc) return;
|
||||
|
||||
const tocLinks = pageToc.querySelectorAll(".page-toc-list a");
|
||||
const content = document.querySelector(".content");
|
||||
if (!tocLinks.length || !content) return;
|
||||
|
||||
const headings = Array.from(
|
||||
content.querySelectorAll("h1[id], h2[id], h3[id]"),
|
||||
);
|
||||
|
||||
if (!headings.length) return;
|
||||
|
||||
// Build a map of heading IDs to TOC links for quick lookup
|
||||
const linkMap = new Map();
|
||||
tocLinks.forEach((link) => {
|
||||
const href = link.getAttribute("href");
|
||||
if (href && href.startsWith("#")) {
|
||||
linkMap.set(href.slice(1), link);
|
||||
}
|
||||
});
|
||||
|
||||
let activeLink = null;
|
||||
|
||||
// Update active link based on scroll position
|
||||
function updateActiveLink() {
|
||||
const threshold = 120; // threshold from the top of the viewport
|
||||
|
||||
let currentHeading = null;
|
||||
|
||||
// Find the last heading that is at or above the threshold
|
||||
for (const heading of headings) {
|
||||
const rect = heading.getBoundingClientRect();
|
||||
if (rect.top <= threshold) {
|
||||
currentHeading = heading;
|
||||
}
|
||||
}
|
||||
|
||||
// If no heading is above threshold, use first heading if it's in view
|
||||
if (!currentHeading && headings.length > 0) {
|
||||
const firstRect = headings[0].getBoundingClientRect();
|
||||
if (firstRect.top < window.innerHeight) {
|
||||
currentHeading = headings[0];
|
||||
}
|
||||
}
|
||||
|
||||
const newLink = currentHeading ? linkMap.get(currentHeading.id) : null;
|
||||
|
||||
if (newLink !== activeLink) {
|
||||
if (activeLink) {
|
||||
activeLink.classList.remove("active");
|
||||
}
|
||||
if (newLink) {
|
||||
newLink.classList.add("active");
|
||||
}
|
||||
activeLink = newLink;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll event handler
|
||||
let ticking = false;
|
||||
function onScroll() {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
updateActiveLink();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
// Also update on hash change (direct link navigation)
|
||||
window.addEventListener("hashchange", () => {
|
||||
requestAnimationFrame(updateActiveLink);
|
||||
});
|
||||
|
||||
// Set initial active state after a small delay to ensure
|
||||
// browser has completed any hash-based scrolling
|
||||
setTimeout(updateActiveLink, 100);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Apply sidebar state immediately before DOM rendering
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
document.body.classList.add("sidebar-collapsed");
|
||||
}
|
||||
|
||||
if (!document.querySelector(".mobile-sidebar-fab")) {
|
||||
createMobileElements();
|
||||
}
|
||||
|
||||
// Initialize collapsible sidebar sections
|
||||
// after mobile elements are created
|
||||
initCollapsibleSections();
|
||||
|
||||
// Initialize scroll spy for page TOC
|
||||
initScrollSpy();
|
||||
|
||||
// Desktop Sidebar Toggle
|
||||
const sidebarToggle = document.querySelector(".sidebar-toggle");
|
||||
|
||||
// On page load, sync the state from `documentElement` to `body`
|
||||
if (document.documentElement.classList.contains("sidebar-collapsed")) {
|
||||
document.body.classList.add("sidebar-collapsed");
|
||||
}
|
||||
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener("click", function () {
|
||||
// Toggle on both elements for consistency
|
||||
document.documentElement.classList.toggle("sidebar-collapsed");
|
||||
document.body.classList.toggle("sidebar-collapsed");
|
||||
|
||||
// Use documentElement to check state and save to localStorage
|
||||
const isCollapsed = document.documentElement.classList.contains(
|
||||
"sidebar-collapsed",
|
||||
);
|
||||
localStorage.setItem("sidebar-collapsed", isCollapsed);
|
||||
});
|
||||
}
|
||||
|
||||
// Make headings clickable for anchor links
|
||||
const content = document.querySelector(".content");
|
||||
if (content) {
|
||||
const headings = content.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
||||
|
||||
headings.forEach(function (heading) {
|
||||
// Generate a valid, unique ID for each heading
|
||||
if (!heading.id) {
|
||||
let baseId = heading.textContent
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-_]/g, "") // remove invalid chars
|
||||
.replace(/^[^a-z]+/, "") // remove leading non-letters
|
||||
.replace(/[\s-_]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") // trim leading/trailing dashes
|
||||
.trim();
|
||||
if (!baseId) {
|
||||
baseId = "section";
|
||||
}
|
||||
let id = baseId;
|
||||
let counter = 1;
|
||||
while (document.getElementById(id)) {
|
||||
id = `${baseId}-${counter++}`;
|
||||
}
|
||||
heading.id = id;
|
||||
}
|
||||
|
||||
// Make the entire heading clickable
|
||||
heading.addEventListener("click", function () {
|
||||
const id = this.id;
|
||||
history.pushState(null, null, "#" + id);
|
||||
|
||||
// Scroll with offset
|
||||
const offset = this.getBoundingClientRect().top + window.scrollY - 80;
|
||||
window.scrollTo({
|
||||
top: offset,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Process footnotes
|
||||
if (content) {
|
||||
const footnoteContainer = document.querySelector(".footnotes-container");
|
||||
|
||||
// Find all footnote references and create a footnotes section
|
||||
const footnoteRefs = content.querySelectorAll('a[href^="#fn"]');
|
||||
if (footnoteRefs.length > 0) {
|
||||
const footnotesDiv = document.createElement("div");
|
||||
footnotesDiv.className = "footnotes";
|
||||
|
||||
const footnotesHeading = document.createElement("h2");
|
||||
footnotesHeading.textContent = "Footnotes";
|
||||
footnotesDiv.appendChild(footnotesHeading);
|
||||
|
||||
const footnotesList = document.createElement("ol");
|
||||
footnoteContainer.appendChild(footnotesDiv);
|
||||
footnotesDiv.appendChild(footnotesList);
|
||||
|
||||
// Add footnotes
|
||||
document.querySelectorAll(".footnote").forEach((footnote) => {
|
||||
const id = footnote.id;
|
||||
const content = footnote.innerHTML;
|
||||
|
||||
const li = document.createElement("li");
|
||||
li.id = id;
|
||||
li.innerHTML = content;
|
||||
|
||||
// Add backlink
|
||||
const backlink = document.createElement("a");
|
||||
backlink.href = "#fnref:" + id.replace("fn:", "");
|
||||
backlink.className = "footnote-backlink";
|
||||
backlink.textContent = "↩";
|
||||
li.appendChild(backlink);
|
||||
|
||||
footnotesList.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Copy link functionality
|
||||
document.querySelectorAll(".copy-link").forEach(function (copyLink) {
|
||||
copyLink.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Get option ID from parent element
|
||||
const option = copyLink.closest(".option");
|
||||
const optionId = option.id;
|
||||
|
||||
// Create URL with hash
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = optionId;
|
||||
|
||||
// Copy to clipboard
|
||||
navigator.clipboard
|
||||
.writeText(url.toString())
|
||||
.then(function () {
|
||||
// Show feedback
|
||||
const feedback = copyLink.nextElementSibling;
|
||||
feedback.style.display = "inline";
|
||||
|
||||
// Hide after 2 seconds
|
||||
setTimeout(function () {
|
||||
feedback.style.display = "none";
|
||||
}, 2000);
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("Could not copy link: ", err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Handle initial hash navigation
|
||||
function scrollToElement(element) {
|
||||
if (element) {
|
||||
const offset = element.getBoundingClientRect().top + window.scrollY - 80;
|
||||
window.scrollTo({
|
||||
top: offset,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (window.location.hash) {
|
||||
const targetElement = document.querySelector(window.location.hash);
|
||||
if (targetElement) {
|
||||
setTimeout(() => scrollToElement(targetElement), 0);
|
||||
// Add highlight class for options page
|
||||
if (targetElement.classList.contains("option")) {
|
||||
targetElement.classList.add("highlight");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile Sidebar Functionality
|
||||
const mobileSidebarContainer = document.querySelector(
|
||||
".mobile-sidebar-container",
|
||||
);
|
||||
const mobileSidebarFab = document.querySelector(".mobile-sidebar-fab");
|
||||
const mobileSidebarHandle = document.querySelector(".mobile-sidebar-handle");
|
||||
|
||||
// Always set up FAB if it exists
|
||||
if (mobileSidebarFab && mobileSidebarContainer) {
|
||||
const openMobileSidebar = () => {
|
||||
mobileSidebarContainer.classList.add("active");
|
||||
mobileSidebarFab.setAttribute("aria-expanded", "true");
|
||||
mobileSidebarContainer.setAttribute("aria-hidden", "false");
|
||||
mobileSidebarFab.classList.add("fab-hidden"); // hide FAB when drawer is open
|
||||
};
|
||||
|
||||
const closeMobileSidebar = () => {
|
||||
mobileSidebarContainer.classList.remove("active");
|
||||
mobileSidebarFab.setAttribute("aria-expanded", "false");
|
||||
mobileSidebarContainer.setAttribute("aria-hidden", "true");
|
||||
mobileSidebarFab.classList.remove("fab-hidden"); // Show FAB when drawer is closed
|
||||
};
|
||||
|
||||
mobileSidebarFab.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (mobileSidebarContainer.classList.contains("active")) {
|
||||
closeMobileSidebar();
|
||||
} else {
|
||||
openMobileSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Only set up drag functionality if handle exists
|
||||
if (mobileSidebarHandle) {
|
||||
// Drag functionality
|
||||
let isDragging = false;
|
||||
let startY = 0;
|
||||
let startHeight = 0;
|
||||
|
||||
// Cleanup function for drag interruption
|
||||
function cleanupDrag() {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
mobileSidebarHandle.style.cursor = "grab";
|
||||
document.body.style.userSelect = "";
|
||||
}
|
||||
}
|
||||
|
||||
mobileSidebarHandle.addEventListener("mousedown", (e) => {
|
||||
isDragging = true;
|
||||
startY = e.pageY;
|
||||
startHeight = mobileSidebarContainer.offsetHeight;
|
||||
mobileSidebarHandle.style.cursor = "grabbing";
|
||||
document.body.style.userSelect = "none"; // prevent text selection
|
||||
});
|
||||
|
||||
mobileSidebarHandle.addEventListener("touchstart", (e) => {
|
||||
isDragging = true;
|
||||
startY = e.touches[0].pageY;
|
||||
startHeight = mobileSidebarContainer.offsetHeight;
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
if (!isDragging) return;
|
||||
const deltaY = startY - e.pageY;
|
||||
const newHeight = startHeight + deltaY;
|
||||
const vh = window.innerHeight;
|
||||
const minHeight = vh * 0.15;
|
||||
const maxHeight = vh * 0.9;
|
||||
|
||||
if (newHeight >= minHeight && newHeight <= maxHeight) {
|
||||
mobileSidebarContainer.style.height = `${newHeight}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("touchmove", (e) => {
|
||||
if (!isDragging) return;
|
||||
const deltaY = startY - e.touches[0].pageY;
|
||||
const newHeight = startHeight + deltaY;
|
||||
const vh = window.innerHeight;
|
||||
const minHeight = vh * 0.15;
|
||||
const maxHeight = vh * 0.9;
|
||||
|
||||
if (newHeight >= minHeight && newHeight <= maxHeight) {
|
||||
mobileSidebarContainer.style.height = `${newHeight}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mouseup", cleanupDrag);
|
||||
document.addEventListener("touchend", cleanupDrag);
|
||||
window.addEventListener("blur", cleanupDrag);
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (document.hidden) cleanupDrag();
|
||||
});
|
||||
}
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener("click", (event) => {
|
||||
if (
|
||||
mobileSidebarContainer.classList.contains("active") &&
|
||||
!mobileSidebarContainer.contains(event.target) &&
|
||||
!mobileSidebarFab.contains(event.target)
|
||||
) {
|
||||
closeMobileSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on escape key
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (
|
||||
event.key === "Escape" &&
|
||||
mobileSidebarContainer.classList.contains("active")
|
||||
) {
|
||||
closeMobileSidebar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Options filter functionality
|
||||
const optionsFilter = document.getElementById("options-filter");
|
||||
if (optionsFilter) {
|
||||
const optionsContainer = document.querySelector(".options-container");
|
||||
if (!optionsContainer) return;
|
||||
|
||||
// Only inject the style if it doesn't already exist
|
||||
if (!document.head.querySelector("style[data-options-hidden]")) {
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.setAttribute("data-options-hidden", "");
|
||||
styleEl.textContent = ".option-hidden{display:none!important}";
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
// Create filter results counter
|
||||
const filterResults = document.createElement("div");
|
||||
filterResults.className = "filter-results";
|
||||
optionsFilter.parentNode.insertBefore(
|
||||
filterResults,
|
||||
optionsFilter.nextSibling,
|
||||
);
|
||||
|
||||
// Detect if we're on a mobile device
|
||||
const isMobile = window.innerWidth < 768 ||
|
||||
/Mobi|Android/i.test(navigator.userAgent);
|
||||
|
||||
// Cache all option elements and their searchable content
|
||||
const options = Array.from(document.querySelectorAll(".option"));
|
||||
const totalCount = options.length;
|
||||
|
||||
// Store the original order of option elements
|
||||
const originalOptionOrder = options.slice();
|
||||
|
||||
// Pre-process and optimize searchable content
|
||||
const optionsData = options.map((option) => {
|
||||
const nameElem = option.querySelector(".option-name");
|
||||
const descriptionElem = option.querySelector(".option-description");
|
||||
const id = option.id ? option.id.toLowerCase() : "";
|
||||
const name = nameElem ? nameElem.textContent.toLowerCase() : "";
|
||||
const description = descriptionElem
|
||||
? descriptionElem.textContent.toLowerCase()
|
||||
: "";
|
||||
|
||||
// Extract keywords for faster searching
|
||||
const keywords = (id + " " + name + " " + description)
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((word) => word.length > 1);
|
||||
|
||||
return {
|
||||
element: option,
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
keywords,
|
||||
searchText: (id + " " + name + " " + description).toLowerCase(),
|
||||
};
|
||||
});
|
||||
|
||||
// Chunk size and rendering variables
|
||||
const CHUNK_SIZE = isMobile ? 15 : 40;
|
||||
let pendingRender = null;
|
||||
let currentChunk = 0;
|
||||
let itemsToProcess = [];
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Process options in chunks to prevent UI freezing
|
||||
function processNextChunk() {
|
||||
const startIdx = currentChunk * CHUNK_SIZE;
|
||||
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
|
||||
|
||||
if (startIdx < itemsToProcess.length) {
|
||||
// Process current chunk
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
const item = itemsToProcess[i];
|
||||
if (item.visible) {
|
||||
item.element.classList.remove("option-hidden");
|
||||
} else {
|
||||
item.element.classList.add("option-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
currentChunk++;
|
||||
pendingRender = requestAnimationFrame(processNextChunk);
|
||||
} else {
|
||||
// Finished processing all chunks
|
||||
pendingRender = null;
|
||||
currentChunk = 0;
|
||||
itemsToProcess = [];
|
||||
|
||||
// Update counter at the very end for best performance
|
||||
if (filterResults.visibleCount !== undefined) {
|
||||
if (filterResults.visibleCount < totalCount) {
|
||||
filterResults.textContent =
|
||||
`Showing ${filterResults.visibleCount} of ${totalCount} options`;
|
||||
filterResults.style.display = "block";
|
||||
} else {
|
||||
filterResults.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterOptions() {
|
||||
const searchTerm = optionsFilter.value.toLowerCase().trim();
|
||||
|
||||
if (pendingRender) {
|
||||
cancelAnimationFrame(pendingRender);
|
||||
pendingRender = null;
|
||||
}
|
||||
currentChunk = 0;
|
||||
itemsToProcess = [];
|
||||
|
||||
if (searchTerm === "") {
|
||||
// Restore original DOM order when filter is cleared
|
||||
const fragment = document.createDocumentFragment();
|
||||
originalOptionOrder.forEach((option) => {
|
||||
option.classList.remove("option-hidden");
|
||||
fragment.appendChild(option);
|
||||
});
|
||||
optionsContainer.appendChild(fragment);
|
||||
filterResults.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const searchTerms = searchTerm
|
||||
.split(/\s+/)
|
||||
.filter((term) => term.length > 0);
|
||||
let visibleCount = 0;
|
||||
|
||||
const titleMatches = [];
|
||||
const descMatches = [];
|
||||
optionsData.forEach((data) => {
|
||||
let isTitleMatch = false;
|
||||
let isDescMatch = false;
|
||||
if (searchTerms.length === 1) {
|
||||
const term = searchTerms[0];
|
||||
isTitleMatch = data.name.includes(term);
|
||||
isDescMatch = !isTitleMatch && data.description.includes(term);
|
||||
} else {
|
||||
isTitleMatch = searchTerms.every((term) => data.name.includes(term));
|
||||
isDescMatch = !isTitleMatch &&
|
||||
searchTerms.every((term) => data.description.includes(term));
|
||||
}
|
||||
if (isTitleMatch) {
|
||||
titleMatches.push(data);
|
||||
} else if (isDescMatch) {
|
||||
descMatches.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
if (searchTerms.length === 1) {
|
||||
const term = searchTerms[0];
|
||||
titleMatches.sort(
|
||||
(a, b) => a.name.indexOf(term) - b.name.indexOf(term),
|
||||
);
|
||||
descMatches.sort(
|
||||
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
|
||||
);
|
||||
}
|
||||
|
||||
itemsToProcess = [];
|
||||
titleMatches.forEach((data) => {
|
||||
visibleCount++;
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
});
|
||||
descMatches.forEach((data) => {
|
||||
visibleCount++;
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
});
|
||||
optionsData.forEach((data) => {
|
||||
if (!itemsToProcess.some((item) => item.element === data.element)) {
|
||||
itemsToProcess.push({ element: data.element, visible: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Reorder DOM so all title matches, then desc matches, then hidden
|
||||
const fragment = document.createDocumentFragment();
|
||||
itemsToProcess.forEach((item) => {
|
||||
fragment.appendChild(item.element);
|
||||
});
|
||||
optionsContainer.appendChild(fragment);
|
||||
|
||||
filterResults.visibleCount = visibleCount;
|
||||
pendingRender = requestAnimationFrame(processNextChunk);
|
||||
}
|
||||
|
||||
// Use different debounce times for desktop vs mobile
|
||||
const debouncedFilter = debounce(filterOptions, isMobile ? 200 : 100);
|
||||
|
||||
// Set up event listeners
|
||||
optionsFilter.addEventListener("input", debouncedFilter);
|
||||
optionsFilter.addEventListener("change", filterOptions);
|
||||
|
||||
// Allow clearing with Escape key
|
||||
optionsFilter.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") {
|
||||
optionsFilter.value = "";
|
||||
filterOptions();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle visibility changes
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (!document.hidden && optionsFilter.value) {
|
||||
filterOptions();
|
||||
}
|
||||
});
|
||||
|
||||
// Initially trigger filter if there's a value
|
||||
if (optionsFilter.value) {
|
||||
filterOptions();
|
||||
}
|
||||
|
||||
// Pre-calculate heights for smoother scrolling
|
||||
if (isMobile && totalCount > 50) {
|
||||
requestIdleCallback(() => {
|
||||
const sampleOption = options[0];
|
||||
if (sampleOption) {
|
||||
const height = sampleOption.offsetHeight;
|
||||
if (height > 0) {
|
||||
options.forEach((opt) => {
|
||||
opt.style.containIntrinsicSize = `0 ${height}px`;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,298 +0,0 @@
|
|||
const isWordBoundary = (char) =>
|
||||
/[A-Z]/.test(char) || /[-_\/.]/.test(char) || /\s/.test(char);
|
||||
|
||||
const isCaseTransition = (prev, curr) => {
|
||||
const prevIsUpper = prev.toLowerCase() !== prev;
|
||||
const currIsUpper = curr.toLowerCase() !== curr;
|
||||
return (
|
||||
prevIsUpper && currIsUpper && prev.toLowerCase() !== curr.toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
const findBestSubsequenceMatch = (query, target) => {
|
||||
const n = query.length;
|
||||
const m = target.length;
|
||||
|
||||
if (n === 0 || m === 0) return null;
|
||||
|
||||
const positions = [];
|
||||
|
||||
const memo = new Map();
|
||||
const key = (qIdx, tIdx, gap) => `${qIdx}:${tIdx}:${gap}`;
|
||||
|
||||
const findBest = (qIdx, tIdx, currentGap) => {
|
||||
if (qIdx === n) {
|
||||
return { done: true, positions: [...positions], gap: currentGap };
|
||||
}
|
||||
|
||||
const memoKey = key(qIdx, tIdx, currentGap);
|
||||
if (memo.has(memoKey)) {
|
||||
return memo.get(memoKey);
|
||||
}
|
||||
|
||||
let bestResult = null;
|
||||
|
||||
for (let i = tIdx; i < m; i++) {
|
||||
if (target[i] === query[qIdx]) {
|
||||
positions.push(i);
|
||||
const gap = qIdx === 0 ? 0 : i - positions[positions.length - 2] - 1;
|
||||
const newGap = currentGap + gap;
|
||||
|
||||
if (newGap > m) {
|
||||
positions.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = findBest(qIdx + 1, i + 1, newGap);
|
||||
positions.pop();
|
||||
|
||||
if (result && (!bestResult || result.gap < bestResult.gap)) {
|
||||
bestResult = result;
|
||||
if (result.gap === 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memo.set(memoKey, bestResult);
|
||||
return bestResult;
|
||||
};
|
||||
|
||||
const result = findBest(0, 0, 0);
|
||||
if (!result) return null;
|
||||
|
||||
const consecutive = (() => {
|
||||
let c = 1;
|
||||
for (let i = 1; i < result.positions.length; i++) {
|
||||
if (result.positions[i] === result.positions[i - 1] + 1) {
|
||||
c++;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
})();
|
||||
|
||||
return {
|
||||
positions: result.positions,
|
||||
consecutive,
|
||||
score: calculateMatchScore(query, target, result.positions, consecutive),
|
||||
};
|
||||
};
|
||||
|
||||
const calculateMatchScore = (query, target, positions, consecutive) => {
|
||||
const n = positions.length;
|
||||
const m = target.length;
|
||||
|
||||
if (n === 0) return 0;
|
||||
|
||||
let score = 1.0;
|
||||
|
||||
const startBonus = (m - positions[0]) / m;
|
||||
score += startBonus * 0.5;
|
||||
|
||||
let gapPenalty = 0;
|
||||
for (let i = 1; i < n; i++) {
|
||||
const gap = positions[i] - positions[i - 1] - 1;
|
||||
if (gap > 0) {
|
||||
gapPenalty += Math.min(gap / m, 1.0) * 0.3;
|
||||
}
|
||||
}
|
||||
score -= gapPenalty;
|
||||
|
||||
const consecutiveBonus = consecutive / n;
|
||||
score += consecutiveBonus * 0.3;
|
||||
|
||||
let boundaryBonus = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const char = target[positions[i]];
|
||||
if (i === 0 || isWordBoundary(char)) {
|
||||
boundaryBonus += 0.05;
|
||||
}
|
||||
if (i > 0) {
|
||||
const prevChar = target[positions[i - 1]];
|
||||
if (isCaseTransition(prevChar, char)) {
|
||||
boundaryBonus += 0.03;
|
||||
}
|
||||
}
|
||||
}
|
||||
score = Math.min(1.0, score + boundaryBonus);
|
||||
|
||||
const lengthPenalty = Math.abs(query.length - n) / Math.max(query.length, m);
|
||||
score -= lengthPenalty * 0.2;
|
||||
|
||||
return Math.max(0, Math.min(1.0, score));
|
||||
};
|
||||
|
||||
const fuzzyMatch = (query, target) => {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const lowerTarget = target.toLowerCase();
|
||||
|
||||
if (lowerQuery.length === 0) return null;
|
||||
if (lowerTarget.length === 0) return null;
|
||||
|
||||
if (lowerTarget === lowerQuery) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
if (lowerTarget.includes(lowerQuery)) {
|
||||
const ratio = lowerQuery.length / lowerTarget.length;
|
||||
return 0.8 + ratio * 0.2;
|
||||
}
|
||||
|
||||
const match = findBestSubsequenceMatch(lowerQuery, lowerTarget);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.min(1.0, match.score);
|
||||
};
|
||||
|
||||
self.onmessage = function (e) {
|
||||
const { messageId, type, data } = e.data;
|
||||
|
||||
const respond = (type, data) => {
|
||||
self.postMessage({ messageId, type, data });
|
||||
};
|
||||
|
||||
const respondError = (error) => {
|
||||
self.postMessage({
|
||||
messageId,
|
||||
type: "error",
|
||||
error: error.message || String(error),
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
if (type === "tokenize") {
|
||||
const text = typeof data === "string" ? data : "";
|
||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||
const tokens = words.filter((word) => word.length > 2);
|
||||
const uniqueTokens = Array.from(new Set(tokens));
|
||||
respond("tokens", uniqueTokens);
|
||||
} else if (type === "search") {
|
||||
const { query, limit = 10 } = data;
|
||||
|
||||
if (!query || typeof query !== "string") {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawQuery = query.toLowerCase();
|
||||
const text = typeof query === "string" ? query : "";
|
||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||
const searchTerms = words.filter((word) => word.length > 2);
|
||||
|
||||
let documents = [];
|
||||
if (typeof data.documents === "string") {
|
||||
documents = JSON.parse(data.documents);
|
||||
} else if (Array.isArray(data.documents)) {
|
||||
documents = data.documents;
|
||||
} else if (typeof data.transferables === "string") {
|
||||
documents = JSON.parse(data.transferables);
|
||||
}
|
||||
|
||||
if (!Array.isArray(documents) || documents.length === 0) {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const useFuzzySearch = rawQuery.length >= 3;
|
||||
|
||||
if (searchTerms.length === 0 && rawQuery.length < 3) {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const pageMatches = new Map();
|
||||
|
||||
// Pre-compute lower-case strings for each document
|
||||
const processedDocs = documents.map((doc, docId) => {
|
||||
const title = typeof doc.title === "string" ? doc.title : "";
|
||||
const content = typeof doc.content === "string" ? doc.content : "";
|
||||
|
||||
return {
|
||||
docId,
|
||||
doc,
|
||||
lowerTitle: title.toLowerCase(),
|
||||
lowerContent: content.toLowerCase(),
|
||||
};
|
||||
});
|
||||
|
||||
// First pass: Score pages with fuzzy matching
|
||||
processedDocs.forEach(({ docId, doc, lowerTitle, lowerContent }) => {
|
||||
let match = pageMatches.get(docId);
|
||||
if (!match) {
|
||||
match = { doc, pageScore: 0, matchingAnchors: [] };
|
||||
pageMatches.set(docId, match);
|
||||
}
|
||||
|
||||
if (useFuzzySearch) {
|
||||
const fuzzyTitleScore = fuzzyMatch(rawQuery, lowerTitle);
|
||||
if (fuzzyTitleScore !== null) {
|
||||
match.pageScore += fuzzyTitleScore * 100;
|
||||
}
|
||||
|
||||
const fuzzyContentScore = fuzzyMatch(rawQuery, lowerContent);
|
||||
if (fuzzyContentScore !== null) {
|
||||
match.pageScore += fuzzyContentScore * 30;
|
||||
}
|
||||
}
|
||||
|
||||
// Token-based exact matching
|
||||
searchTerms.forEach((term) => {
|
||||
if (lowerTitle.includes(term)) {
|
||||
match.pageScore += lowerTitle === term ? 20 : 10;
|
||||
}
|
||||
if (lowerContent.includes(term)) {
|
||||
match.pageScore += 2;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Second pass: Find matching anchors
|
||||
pageMatches.forEach((match) => {
|
||||
const doc = match.doc;
|
||||
if (
|
||||
!doc.anchors ||
|
||||
!Array.isArray(doc.anchors) ||
|
||||
doc.anchors.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.anchors.forEach((anchor) => {
|
||||
if (!anchor || !anchor.text) return;
|
||||
|
||||
const anchorText = anchor.text.toLowerCase();
|
||||
let anchorMatches = false;
|
||||
|
||||
if (useFuzzySearch) {
|
||||
const fuzzyScore = fuzzyMatch(rawQuery, anchorText);
|
||||
if (fuzzyScore !== null && fuzzyScore >= 0.4) {
|
||||
anchorMatches = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anchorMatches) {
|
||||
searchTerms.forEach((term) => {
|
||||
if (anchorText.includes(term)) {
|
||||
anchorMatches = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (anchorMatches) {
|
||||
match.matchingAnchors.push(anchor);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const results = Array.from(pageMatches.values())
|
||||
.filter((m) => m.pageScore > 5)
|
||||
.sort((a, b) => b.pageScore - a.pageScore)
|
||||
.slice(0, limit);
|
||||
|
||||
respond("results", results);
|
||||
}
|
||||
} catch (error) {
|
||||
respondError(error);
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -1,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 & 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 & Suggestions</h2>
|
||||
<p>Some quirks are not exactly quirks, but bugs in the module system. If you notice
|
||||
any issues with <strong>nvf</strong>, or this documentation, then please consider reporting
|
||||
them over at the <a href="https://github.com/notashelf/nvf/issues">issue tracker</a>. Issues tab, in addition to the
|
||||
<a href="https://github.com/notashelf/nvf/discussions">discussions tab</a> is a good place as any to request new features.</p>
|
||||
<p>You may also consider submitting bug fixes, feature additions and upstreamed
|
||||
changes that you think are critical over at the <a href="https://github.com/notashelf/nvf/pulls">pull requests tab</a>.</p>
|
||||
</body></html></main>
|
||||
</div>
|
||||
|
||||
<aside class="page-toc">
|
||||
<nav class="page-toc-nav">
|
||||
<h3>On this page</h3>
|
||||
<ul class="page-toc-list">
|
||||
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
|
||||
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
|
||||
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
|
||||
</ul><li><a href="#ch-bugs-suggestions">Bugs & Suggestions</a>
|
||||
</li></ul></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<footer>
|
||||
<p>Generated with ndg</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,110 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NVF - Search</title>
|
||||
|
||||
<script>
|
||||
// Apply sidebar state immediately to prevent flash
|
||||
(function () {
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/style.css" />
|
||||
<script defer src="assets/main.js"></script>
|
||||
<script>
|
||||
window.searchNamespace = window.searchNamespace || {};
|
||||
window.searchNamespace.rootPath = "";
|
||||
</script>
|
||||
<script defer src="assets/search.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-left">
|
||||
<h1 class="site-title">
|
||||
<a href="index.html">NVF</a>
|
||||
</h1>
|
||||
</div>
|
||||
<nav class="header-nav">
|
||||
<ul>
|
||||
<li >
|
||||
<a href="options.html">Options</a>
|
||||
</li>
|
||||
|
||||
<li><a href="search.html">Search</a></li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="search-container">
|
||||
<input type="text" id="search-input" placeholder="Search..." />
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<div class="sidebar-toggle" aria-label="Toggle sidebar">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<nav id="sidebar" class="sidebar">
|
||||
<div class="docs-nav">
|
||||
<h2>Documents</h2>
|
||||
<ul>
|
||||
<li><a href="index.html">Introduction</a></li>
|
||||
<li><a href="configuring.html">Configuring nvf</a></li>
|
||||
<li><a href="hacking.html">Hacking nvf</a></li>
|
||||
<li><a href="tips.html">Helpful Tips</a></li>
|
||||
<li><a href="quirks.html">Known Issues and Quirks</a></li>
|
||||
<li><a href="release-notes.html">Release Notes</a></li>
|
||||
<li><a href="search.html">Search</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="toc">
|
||||
<h2>Contents</h2>
|
||||
<ul class="toc-list">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
<h1>Search</h1>
|
||||
<div class="search-page">
|
||||
<div class="search-form">
|
||||
<input
|
||||
type="text"
|
||||
id="search-page-input"
|
||||
placeholder="Search..."
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<div id="search-page-results" class="search-page-results"></div>
|
||||
</div>
|
||||
<div class="footnotes-container">
|
||||
<!-- Footnotes will be appended here -->
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Generated with ndg</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,740 +0,0 @@
|
|||
// Polyfill for requestIdleCallback for Safari and unsupported browsers
|
||||
if (typeof window.requestIdleCallback === "undefined") {
|
||||
window.requestIdleCallback = function (cb) {
|
||||
const start = Date.now();
|
||||
const idlePeriod = 50;
|
||||
return setTimeout(function () {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
timeRemaining: function () {
|
||||
return Math.max(0, idlePeriod - (Date.now() - start));
|
||||
},
|
||||
});
|
||||
}, 1);
|
||||
};
|
||||
window.cancelIdleCallback = function (id) {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}
|
||||
|
||||
// Create mobile elements if they don't exist
|
||||
function createMobileElements() {
|
||||
// Create mobile sidebar FAB
|
||||
const mobileFab = document.createElement("button");
|
||||
mobileFab.className = "mobile-sidebar-fab";
|
||||
mobileFab.setAttribute("aria-label", "Toggle sidebar menu");
|
||||
mobileFab.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Only show FAB on mobile (max-width: 800px)
|
||||
function updateFabVisibility() {
|
||||
if (window.innerWidth > 800) {
|
||||
if (mobileFab.parentNode) mobileFab.parentNode.removeChild(mobileFab);
|
||||
} else {
|
||||
if (!document.body.contains(mobileFab)) {
|
||||
document.body.appendChild(mobileFab);
|
||||
}
|
||||
mobileFab.style.display = "flex";
|
||||
}
|
||||
}
|
||||
updateFabVisibility();
|
||||
window.addEventListener("resize", updateFabVisibility);
|
||||
|
||||
// Create mobile sidebar container
|
||||
const mobileContainer = document.createElement("div");
|
||||
mobileContainer.className = "mobile-sidebar-container";
|
||||
mobileContainer.innerHTML = `
|
||||
<div class="mobile-sidebar-handle">
|
||||
<div class="mobile-sidebar-dragger"></div>
|
||||
</div>
|
||||
<div class="mobile-sidebar-content">
|
||||
<!-- Sidebar content will be cloned here -->
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create mobile search popup
|
||||
const mobileSearchPopup = document.createElement("div");
|
||||
mobileSearchPopup.id = "mobile-search-popup";
|
||||
mobileSearchPopup.className = "mobile-search-popup";
|
||||
mobileSearchPopup.innerHTML = `
|
||||
<div class="mobile-search-container">
|
||||
<div class="mobile-search-header">
|
||||
<input type="text" id="mobile-search-input" placeholder="Search..." />
|
||||
<button id="close-mobile-search" class="close-mobile-search" aria-label="Close search">×</button>
|
||||
</div>
|
||||
<div id="mobile-search-results" class="mobile-search-results"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert at end of body so it is not affected by .container flex or stacking context
|
||||
document.body.appendChild(mobileContainer);
|
||||
document.body.appendChild(mobileSearchPopup);
|
||||
|
||||
// Immediately populate mobile sidebar content if desktop sidebar exists
|
||||
const desktopSidebar = document.querySelector(".sidebar");
|
||||
const mobileSidebarContent = mobileContainer.querySelector(
|
||||
".mobile-sidebar-content",
|
||||
);
|
||||
if (desktopSidebar && mobileSidebarContent) {
|
||||
mobileSidebarContent.innerHTML = desktopSidebar.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize collapsible sidebar sections with state persistence
|
||||
function initCollapsibleSections() {
|
||||
// Target sections in both desktop and mobile sidebars
|
||||
const sections = document.querySelectorAll(
|
||||
".sidebar .sidebar-section, .mobile-sidebar-content .sidebar-section",
|
||||
);
|
||||
|
||||
sections.forEach((section) => {
|
||||
const sectionId = section.dataset.section;
|
||||
if (!sectionId) return;
|
||||
|
||||
const storageKey = `sidebar-section-${sectionId}`;
|
||||
const savedState = localStorage.getItem(storageKey);
|
||||
|
||||
// Restore saved state (default is open)
|
||||
if (savedState === "closed") {
|
||||
section.removeAttribute("open");
|
||||
}
|
||||
|
||||
// Save state on toggle and sync between desktop/mobile
|
||||
section.addEventListener("toggle", () => {
|
||||
localStorage.setItem(storageKey, section.open ? "open" : "closed");
|
||||
|
||||
// Sync state between desktop and mobile versions
|
||||
const allWithSameSection = document.querySelectorAll(
|
||||
`.sidebar-section[data-section="${sectionId}"]`,
|
||||
);
|
||||
allWithSameSection.forEach((el) => {
|
||||
if (el !== section) {
|
||||
el.open = section.open;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize scroll spy
|
||||
function initScrollSpy() {
|
||||
const pageToc = document.querySelector(".page-toc");
|
||||
if (!pageToc) return;
|
||||
|
||||
const tocLinks = pageToc.querySelectorAll(".page-toc-list a");
|
||||
const content = document.querySelector(".content");
|
||||
if (!tocLinks.length || !content) return;
|
||||
|
||||
const headings = Array.from(
|
||||
content.querySelectorAll("h1[id], h2[id], h3[id]"),
|
||||
);
|
||||
|
||||
if (!headings.length) return;
|
||||
|
||||
// Build a map of heading IDs to TOC links for quick lookup
|
||||
const linkMap = new Map();
|
||||
tocLinks.forEach((link) => {
|
||||
const href = link.getAttribute("href");
|
||||
if (href && href.startsWith("#")) {
|
||||
linkMap.set(href.slice(1), link);
|
||||
}
|
||||
});
|
||||
|
||||
let activeLink = null;
|
||||
|
||||
// Update active link based on scroll position
|
||||
function updateActiveLink() {
|
||||
const threshold = 120; // threshold from the top of the viewport
|
||||
|
||||
let currentHeading = null;
|
||||
|
||||
// Find the last heading that is at or above the threshold
|
||||
for (const heading of headings) {
|
||||
const rect = heading.getBoundingClientRect();
|
||||
if (rect.top <= threshold) {
|
||||
currentHeading = heading;
|
||||
}
|
||||
}
|
||||
|
||||
// If no heading is above threshold, use first heading if it's in view
|
||||
if (!currentHeading && headings.length > 0) {
|
||||
const firstRect = headings[0].getBoundingClientRect();
|
||||
if (firstRect.top < window.innerHeight) {
|
||||
currentHeading = headings[0];
|
||||
}
|
||||
}
|
||||
|
||||
const newLink = currentHeading ? linkMap.get(currentHeading.id) : null;
|
||||
|
||||
if (newLink !== activeLink) {
|
||||
if (activeLink) {
|
||||
activeLink.classList.remove("active");
|
||||
}
|
||||
if (newLink) {
|
||||
newLink.classList.add("active");
|
||||
}
|
||||
activeLink = newLink;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll event handler
|
||||
let ticking = false;
|
||||
function onScroll() {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
updateActiveLink();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
// Also update on hash change (direct link navigation)
|
||||
window.addEventListener("hashchange", () => {
|
||||
requestAnimationFrame(updateActiveLink);
|
||||
});
|
||||
|
||||
// Set initial active state after a small delay to ensure
|
||||
// browser has completed any hash-based scrolling
|
||||
setTimeout(updateActiveLink, 100);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Apply sidebar state immediately before DOM rendering
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
document.body.classList.add("sidebar-collapsed");
|
||||
}
|
||||
|
||||
if (!document.querySelector(".mobile-sidebar-fab")) {
|
||||
createMobileElements();
|
||||
}
|
||||
|
||||
// Initialize collapsible sidebar sections
|
||||
// after mobile elements are created
|
||||
initCollapsibleSections();
|
||||
|
||||
// Initialize scroll spy for page TOC
|
||||
initScrollSpy();
|
||||
|
||||
// Desktop Sidebar Toggle
|
||||
const sidebarToggle = document.querySelector(".sidebar-toggle");
|
||||
|
||||
// On page load, sync the state from `documentElement` to `body`
|
||||
if (document.documentElement.classList.contains("sidebar-collapsed")) {
|
||||
document.body.classList.add("sidebar-collapsed");
|
||||
}
|
||||
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener("click", function () {
|
||||
// Toggle on both elements for consistency
|
||||
document.documentElement.classList.toggle("sidebar-collapsed");
|
||||
document.body.classList.toggle("sidebar-collapsed");
|
||||
|
||||
// Use documentElement to check state and save to localStorage
|
||||
const isCollapsed = document.documentElement.classList.contains(
|
||||
"sidebar-collapsed",
|
||||
);
|
||||
localStorage.setItem("sidebar-collapsed", isCollapsed);
|
||||
});
|
||||
}
|
||||
|
||||
// Make headings clickable for anchor links
|
||||
const content = document.querySelector(".content");
|
||||
if (content) {
|
||||
const headings = content.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
||||
|
||||
headings.forEach(function (heading) {
|
||||
// Generate a valid, unique ID for each heading
|
||||
if (!heading.id) {
|
||||
let baseId = heading.textContent
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-_]/g, "") // remove invalid chars
|
||||
.replace(/^[^a-z]+/, "") // remove leading non-letters
|
||||
.replace(/[\s-_]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") // trim leading/trailing dashes
|
||||
.trim();
|
||||
if (!baseId) {
|
||||
baseId = "section";
|
||||
}
|
||||
let id = baseId;
|
||||
let counter = 1;
|
||||
while (document.getElementById(id)) {
|
||||
id = `${baseId}-${counter++}`;
|
||||
}
|
||||
heading.id = id;
|
||||
}
|
||||
|
||||
// Make the entire heading clickable
|
||||
heading.addEventListener("click", function () {
|
||||
const id = this.id;
|
||||
history.pushState(null, null, "#" + id);
|
||||
|
||||
// Scroll with offset
|
||||
const offset = this.getBoundingClientRect().top + window.scrollY - 80;
|
||||
window.scrollTo({
|
||||
top: offset,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Process footnotes
|
||||
if (content) {
|
||||
const footnoteContainer = document.querySelector(".footnotes-container");
|
||||
|
||||
// Find all footnote references and create a footnotes section
|
||||
const footnoteRefs = content.querySelectorAll('a[href^="#fn"]');
|
||||
if (footnoteRefs.length > 0) {
|
||||
const footnotesDiv = document.createElement("div");
|
||||
footnotesDiv.className = "footnotes";
|
||||
|
||||
const footnotesHeading = document.createElement("h2");
|
||||
footnotesHeading.textContent = "Footnotes";
|
||||
footnotesDiv.appendChild(footnotesHeading);
|
||||
|
||||
const footnotesList = document.createElement("ol");
|
||||
footnoteContainer.appendChild(footnotesDiv);
|
||||
footnotesDiv.appendChild(footnotesList);
|
||||
|
||||
// Add footnotes
|
||||
document.querySelectorAll(".footnote").forEach((footnote) => {
|
||||
const id = footnote.id;
|
||||
const content = footnote.innerHTML;
|
||||
|
||||
const li = document.createElement("li");
|
||||
li.id = id;
|
||||
li.innerHTML = content;
|
||||
|
||||
// Add backlink
|
||||
const backlink = document.createElement("a");
|
||||
backlink.href = "#fnref:" + id.replace("fn:", "");
|
||||
backlink.className = "footnote-backlink";
|
||||
backlink.textContent = "↩";
|
||||
li.appendChild(backlink);
|
||||
|
||||
footnotesList.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Copy link functionality
|
||||
document.querySelectorAll(".copy-link").forEach(function (copyLink) {
|
||||
copyLink.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Get option ID from parent element
|
||||
const option = copyLink.closest(".option");
|
||||
const optionId = option.id;
|
||||
|
||||
// Create URL with hash
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = optionId;
|
||||
|
||||
// Copy to clipboard
|
||||
navigator.clipboard
|
||||
.writeText(url.toString())
|
||||
.then(function () {
|
||||
// Show feedback
|
||||
const feedback = copyLink.nextElementSibling;
|
||||
feedback.style.display = "inline";
|
||||
|
||||
// Hide after 2 seconds
|
||||
setTimeout(function () {
|
||||
feedback.style.display = "none";
|
||||
}, 2000);
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("Could not copy link: ", err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Handle initial hash navigation
|
||||
function scrollToElement(element) {
|
||||
if (element) {
|
||||
const offset = element.getBoundingClientRect().top + window.scrollY - 80;
|
||||
window.scrollTo({
|
||||
top: offset,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (window.location.hash) {
|
||||
const targetElement = document.querySelector(window.location.hash);
|
||||
if (targetElement) {
|
||||
setTimeout(() => scrollToElement(targetElement), 0);
|
||||
// Add highlight class for options page
|
||||
if (targetElement.classList.contains("option")) {
|
||||
targetElement.classList.add("highlight");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile Sidebar Functionality
|
||||
const mobileSidebarContainer = document.querySelector(
|
||||
".mobile-sidebar-container",
|
||||
);
|
||||
const mobileSidebarFab = document.querySelector(".mobile-sidebar-fab");
|
||||
const mobileSidebarHandle = document.querySelector(".mobile-sidebar-handle");
|
||||
|
||||
// Always set up FAB if it exists
|
||||
if (mobileSidebarFab && mobileSidebarContainer) {
|
||||
const openMobileSidebar = () => {
|
||||
mobileSidebarContainer.classList.add("active");
|
||||
mobileSidebarFab.setAttribute("aria-expanded", "true");
|
||||
mobileSidebarContainer.setAttribute("aria-hidden", "false");
|
||||
mobileSidebarFab.classList.add("fab-hidden"); // hide FAB when drawer is open
|
||||
};
|
||||
|
||||
const closeMobileSidebar = () => {
|
||||
mobileSidebarContainer.classList.remove("active");
|
||||
mobileSidebarFab.setAttribute("aria-expanded", "false");
|
||||
mobileSidebarContainer.setAttribute("aria-hidden", "true");
|
||||
mobileSidebarFab.classList.remove("fab-hidden"); // Show FAB when drawer is closed
|
||||
};
|
||||
|
||||
mobileSidebarFab.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (mobileSidebarContainer.classList.contains("active")) {
|
||||
closeMobileSidebar();
|
||||
} else {
|
||||
openMobileSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Only set up drag functionality if handle exists
|
||||
if (mobileSidebarHandle) {
|
||||
// Drag functionality
|
||||
let isDragging = false;
|
||||
let startY = 0;
|
||||
let startHeight = 0;
|
||||
|
||||
// Cleanup function for drag interruption
|
||||
function cleanupDrag() {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
mobileSidebarHandle.style.cursor = "grab";
|
||||
document.body.style.userSelect = "";
|
||||
}
|
||||
}
|
||||
|
||||
mobileSidebarHandle.addEventListener("mousedown", (e) => {
|
||||
isDragging = true;
|
||||
startY = e.pageY;
|
||||
startHeight = mobileSidebarContainer.offsetHeight;
|
||||
mobileSidebarHandle.style.cursor = "grabbing";
|
||||
document.body.style.userSelect = "none"; // prevent text selection
|
||||
});
|
||||
|
||||
mobileSidebarHandle.addEventListener("touchstart", (e) => {
|
||||
isDragging = true;
|
||||
startY = e.touches[0].pageY;
|
||||
startHeight = mobileSidebarContainer.offsetHeight;
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
if (!isDragging) return;
|
||||
const deltaY = startY - e.pageY;
|
||||
const newHeight = startHeight + deltaY;
|
||||
const vh = window.innerHeight;
|
||||
const minHeight = vh * 0.15;
|
||||
const maxHeight = vh * 0.9;
|
||||
|
||||
if (newHeight >= minHeight && newHeight <= maxHeight) {
|
||||
mobileSidebarContainer.style.height = `${newHeight}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("touchmove", (e) => {
|
||||
if (!isDragging) return;
|
||||
const deltaY = startY - e.touches[0].pageY;
|
||||
const newHeight = startHeight + deltaY;
|
||||
const vh = window.innerHeight;
|
||||
const minHeight = vh * 0.15;
|
||||
const maxHeight = vh * 0.9;
|
||||
|
||||
if (newHeight >= minHeight && newHeight <= maxHeight) {
|
||||
mobileSidebarContainer.style.height = `${newHeight}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mouseup", cleanupDrag);
|
||||
document.addEventListener("touchend", cleanupDrag);
|
||||
window.addEventListener("blur", cleanupDrag);
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (document.hidden) cleanupDrag();
|
||||
});
|
||||
}
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener("click", (event) => {
|
||||
if (
|
||||
mobileSidebarContainer.classList.contains("active") &&
|
||||
!mobileSidebarContainer.contains(event.target) &&
|
||||
!mobileSidebarFab.contains(event.target)
|
||||
) {
|
||||
closeMobileSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on escape key
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (
|
||||
event.key === "Escape" &&
|
||||
mobileSidebarContainer.classList.contains("active")
|
||||
) {
|
||||
closeMobileSidebar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Options filter functionality
|
||||
const optionsFilter = document.getElementById("options-filter");
|
||||
if (optionsFilter) {
|
||||
const optionsContainer = document.querySelector(".options-container");
|
||||
if (!optionsContainer) return;
|
||||
|
||||
// Only inject the style if it doesn't already exist
|
||||
if (!document.head.querySelector("style[data-options-hidden]")) {
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.setAttribute("data-options-hidden", "");
|
||||
styleEl.textContent = ".option-hidden{display:none!important}";
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
// Create filter results counter
|
||||
const filterResults = document.createElement("div");
|
||||
filterResults.className = "filter-results";
|
||||
optionsFilter.parentNode.insertBefore(
|
||||
filterResults,
|
||||
optionsFilter.nextSibling,
|
||||
);
|
||||
|
||||
// Detect if we're on a mobile device
|
||||
const isMobile = window.innerWidth < 768 ||
|
||||
/Mobi|Android/i.test(navigator.userAgent);
|
||||
|
||||
// Cache all option elements and their searchable content
|
||||
const options = Array.from(document.querySelectorAll(".option"));
|
||||
const totalCount = options.length;
|
||||
|
||||
// Store the original order of option elements
|
||||
const originalOptionOrder = options.slice();
|
||||
|
||||
// Pre-process and optimize searchable content
|
||||
const optionsData = options.map((option) => {
|
||||
const nameElem = option.querySelector(".option-name");
|
||||
const descriptionElem = option.querySelector(".option-description");
|
||||
const id = option.id ? option.id.toLowerCase() : "";
|
||||
const name = nameElem ? nameElem.textContent.toLowerCase() : "";
|
||||
const description = descriptionElem
|
||||
? descriptionElem.textContent.toLowerCase()
|
||||
: "";
|
||||
|
||||
// Extract keywords for faster searching
|
||||
const keywords = (id + " " + name + " " + description)
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((word) => word.length > 1);
|
||||
|
||||
return {
|
||||
element: option,
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
keywords,
|
||||
searchText: (id + " " + name + " " + description).toLowerCase(),
|
||||
};
|
||||
});
|
||||
|
||||
// Chunk size and rendering variables
|
||||
const CHUNK_SIZE = isMobile ? 15 : 40;
|
||||
let pendingRender = null;
|
||||
let currentChunk = 0;
|
||||
let itemsToProcess = [];
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Process options in chunks to prevent UI freezing
|
||||
function processNextChunk() {
|
||||
const startIdx = currentChunk * CHUNK_SIZE;
|
||||
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
|
||||
|
||||
if (startIdx < itemsToProcess.length) {
|
||||
// Process current chunk
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
const item = itemsToProcess[i];
|
||||
if (item.visible) {
|
||||
item.element.classList.remove("option-hidden");
|
||||
} else {
|
||||
item.element.classList.add("option-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
currentChunk++;
|
||||
pendingRender = requestAnimationFrame(processNextChunk);
|
||||
} else {
|
||||
// Finished processing all chunks
|
||||
pendingRender = null;
|
||||
currentChunk = 0;
|
||||
itemsToProcess = [];
|
||||
|
||||
// Update counter at the very end for best performance
|
||||
if (filterResults.visibleCount !== undefined) {
|
||||
if (filterResults.visibleCount < totalCount) {
|
||||
filterResults.textContent =
|
||||
`Showing ${filterResults.visibleCount} of ${totalCount} options`;
|
||||
filterResults.style.display = "block";
|
||||
} else {
|
||||
filterResults.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterOptions() {
|
||||
const searchTerm = optionsFilter.value.toLowerCase().trim();
|
||||
|
||||
if (pendingRender) {
|
||||
cancelAnimationFrame(pendingRender);
|
||||
pendingRender = null;
|
||||
}
|
||||
currentChunk = 0;
|
||||
itemsToProcess = [];
|
||||
|
||||
if (searchTerm === "") {
|
||||
// Restore original DOM order when filter is cleared
|
||||
const fragment = document.createDocumentFragment();
|
||||
originalOptionOrder.forEach((option) => {
|
||||
option.classList.remove("option-hidden");
|
||||
fragment.appendChild(option);
|
||||
});
|
||||
optionsContainer.appendChild(fragment);
|
||||
filterResults.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const searchTerms = searchTerm
|
||||
.split(/\s+/)
|
||||
.filter((term) => term.length > 0);
|
||||
let visibleCount = 0;
|
||||
|
||||
const titleMatches = [];
|
||||
const descMatches = [];
|
||||
optionsData.forEach((data) => {
|
||||
let isTitleMatch = false;
|
||||
let isDescMatch = false;
|
||||
if (searchTerms.length === 1) {
|
||||
const term = searchTerms[0];
|
||||
isTitleMatch = data.name.includes(term);
|
||||
isDescMatch = !isTitleMatch && data.description.includes(term);
|
||||
} else {
|
||||
isTitleMatch = searchTerms.every((term) => data.name.includes(term));
|
||||
isDescMatch = !isTitleMatch &&
|
||||
searchTerms.every((term) => data.description.includes(term));
|
||||
}
|
||||
if (isTitleMatch) {
|
||||
titleMatches.push(data);
|
||||
} else if (isDescMatch) {
|
||||
descMatches.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
if (searchTerms.length === 1) {
|
||||
const term = searchTerms[0];
|
||||
titleMatches.sort(
|
||||
(a, b) => a.name.indexOf(term) - b.name.indexOf(term),
|
||||
);
|
||||
descMatches.sort(
|
||||
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
|
||||
);
|
||||
}
|
||||
|
||||
itemsToProcess = [];
|
||||
titleMatches.forEach((data) => {
|
||||
visibleCount++;
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
});
|
||||
descMatches.forEach((data) => {
|
||||
visibleCount++;
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
});
|
||||
optionsData.forEach((data) => {
|
||||
if (!itemsToProcess.some((item) => item.element === data.element)) {
|
||||
itemsToProcess.push({ element: data.element, visible: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Reorder DOM so all title matches, then desc matches, then hidden
|
||||
const fragment = document.createDocumentFragment();
|
||||
itemsToProcess.forEach((item) => {
|
||||
fragment.appendChild(item.element);
|
||||
});
|
||||
optionsContainer.appendChild(fragment);
|
||||
|
||||
filterResults.visibleCount = visibleCount;
|
||||
pendingRender = requestAnimationFrame(processNextChunk);
|
||||
}
|
||||
|
||||
// Use different debounce times for desktop vs mobile
|
||||
const debouncedFilter = debounce(filterOptions, isMobile ? 200 : 100);
|
||||
|
||||
// Set up event listeners
|
||||
optionsFilter.addEventListener("input", debouncedFilter);
|
||||
optionsFilter.addEventListener("change", filterOptions);
|
||||
|
||||
// Allow clearing with Escape key
|
||||
optionsFilter.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") {
|
||||
optionsFilter.value = "";
|
||||
filterOptions();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle visibility changes
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (!document.hidden && optionsFilter.value) {
|
||||
filterOptions();
|
||||
}
|
||||
});
|
||||
|
||||
// Initially trigger filter if there's a value
|
||||
if (optionsFilter.value) {
|
||||
filterOptions();
|
||||
}
|
||||
|
||||
// Pre-calculate heights for smoother scrolling
|
||||
if (isMobile && totalCount > 50) {
|
||||
requestIdleCallback(() => {
|
||||
const sampleOption = options[0];
|
||||
if (sampleOption) {
|
||||
const height = sampleOption.offsetHeight;
|
||||
if (height > 0) {
|
||||
options.forEach((opt) => {
|
||||
opt.style.containIntrinsicSize = `0 ${height}px`;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,298 +0,0 @@
|
|||
const isWordBoundary = (char) =>
|
||||
/[A-Z]/.test(char) || /[-_\/.]/.test(char) || /\s/.test(char);
|
||||
|
||||
const isCaseTransition = (prev, curr) => {
|
||||
const prevIsUpper = prev.toLowerCase() !== prev;
|
||||
const currIsUpper = curr.toLowerCase() !== curr;
|
||||
return (
|
||||
prevIsUpper && currIsUpper && prev.toLowerCase() !== curr.toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
const findBestSubsequenceMatch = (query, target) => {
|
||||
const n = query.length;
|
||||
const m = target.length;
|
||||
|
||||
if (n === 0 || m === 0) return null;
|
||||
|
||||
const positions = [];
|
||||
|
||||
const memo = new Map();
|
||||
const key = (qIdx, tIdx, gap) => `${qIdx}:${tIdx}:${gap}`;
|
||||
|
||||
const findBest = (qIdx, tIdx, currentGap) => {
|
||||
if (qIdx === n) {
|
||||
return { done: true, positions: [...positions], gap: currentGap };
|
||||
}
|
||||
|
||||
const memoKey = key(qIdx, tIdx, currentGap);
|
||||
if (memo.has(memoKey)) {
|
||||
return memo.get(memoKey);
|
||||
}
|
||||
|
||||
let bestResult = null;
|
||||
|
||||
for (let i = tIdx; i < m; i++) {
|
||||
if (target[i] === query[qIdx]) {
|
||||
positions.push(i);
|
||||
const gap = qIdx === 0 ? 0 : i - positions[positions.length - 2] - 1;
|
||||
const newGap = currentGap + gap;
|
||||
|
||||
if (newGap > m) {
|
||||
positions.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = findBest(qIdx + 1, i + 1, newGap);
|
||||
positions.pop();
|
||||
|
||||
if (result && (!bestResult || result.gap < bestResult.gap)) {
|
||||
bestResult = result;
|
||||
if (result.gap === 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memo.set(memoKey, bestResult);
|
||||
return bestResult;
|
||||
};
|
||||
|
||||
const result = findBest(0, 0, 0);
|
||||
if (!result) return null;
|
||||
|
||||
const consecutive = (() => {
|
||||
let c = 1;
|
||||
for (let i = 1; i < result.positions.length; i++) {
|
||||
if (result.positions[i] === result.positions[i - 1] + 1) {
|
||||
c++;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
})();
|
||||
|
||||
return {
|
||||
positions: result.positions,
|
||||
consecutive,
|
||||
score: calculateMatchScore(query, target, result.positions, consecutive),
|
||||
};
|
||||
};
|
||||
|
||||
const calculateMatchScore = (query, target, positions, consecutive) => {
|
||||
const n = positions.length;
|
||||
const m = target.length;
|
||||
|
||||
if (n === 0) return 0;
|
||||
|
||||
let score = 1.0;
|
||||
|
||||
const startBonus = (m - positions[0]) / m;
|
||||
score += startBonus * 0.5;
|
||||
|
||||
let gapPenalty = 0;
|
||||
for (let i = 1; i < n; i++) {
|
||||
const gap = positions[i] - positions[i - 1] - 1;
|
||||
if (gap > 0) {
|
||||
gapPenalty += Math.min(gap / m, 1.0) * 0.3;
|
||||
}
|
||||
}
|
||||
score -= gapPenalty;
|
||||
|
||||
const consecutiveBonus = consecutive / n;
|
||||
score += consecutiveBonus * 0.3;
|
||||
|
||||
let boundaryBonus = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const char = target[positions[i]];
|
||||
if (i === 0 || isWordBoundary(char)) {
|
||||
boundaryBonus += 0.05;
|
||||
}
|
||||
if (i > 0) {
|
||||
const prevChar = target[positions[i - 1]];
|
||||
if (isCaseTransition(prevChar, char)) {
|
||||
boundaryBonus += 0.03;
|
||||
}
|
||||
}
|
||||
}
|
||||
score = Math.min(1.0, score + boundaryBonus);
|
||||
|
||||
const lengthPenalty = Math.abs(query.length - n) / Math.max(query.length, m);
|
||||
score -= lengthPenalty * 0.2;
|
||||
|
||||
return Math.max(0, Math.min(1.0, score));
|
||||
};
|
||||
|
||||
const fuzzyMatch = (query, target) => {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const lowerTarget = target.toLowerCase();
|
||||
|
||||
if (lowerQuery.length === 0) return null;
|
||||
if (lowerTarget.length === 0) return null;
|
||||
|
||||
if (lowerTarget === lowerQuery) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
if (lowerTarget.includes(lowerQuery)) {
|
||||
const ratio = lowerQuery.length / lowerTarget.length;
|
||||
return 0.8 + ratio * 0.2;
|
||||
}
|
||||
|
||||
const match = findBestSubsequenceMatch(lowerQuery, lowerTarget);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.min(1.0, match.score);
|
||||
};
|
||||
|
||||
self.onmessage = function (e) {
|
||||
const { messageId, type, data } = e.data;
|
||||
|
||||
const respond = (type, data) => {
|
||||
self.postMessage({ messageId, type, data });
|
||||
};
|
||||
|
||||
const respondError = (error) => {
|
||||
self.postMessage({
|
||||
messageId,
|
||||
type: "error",
|
||||
error: error.message || String(error),
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
if (type === "tokenize") {
|
||||
const text = typeof data === "string" ? data : "";
|
||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||
const tokens = words.filter((word) => word.length > 2);
|
||||
const uniqueTokens = Array.from(new Set(tokens));
|
||||
respond("tokens", uniqueTokens);
|
||||
} else if (type === "search") {
|
||||
const { query, limit = 10 } = data;
|
||||
|
||||
if (!query || typeof query !== "string") {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawQuery = query.toLowerCase();
|
||||
const text = typeof query === "string" ? query : "";
|
||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||
const searchTerms = words.filter((word) => word.length > 2);
|
||||
|
||||
let documents = [];
|
||||
if (typeof data.documents === "string") {
|
||||
documents = JSON.parse(data.documents);
|
||||
} else if (Array.isArray(data.documents)) {
|
||||
documents = data.documents;
|
||||
} else if (typeof data.transferables === "string") {
|
||||
documents = JSON.parse(data.transferables);
|
||||
}
|
||||
|
||||
if (!Array.isArray(documents) || documents.length === 0) {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const useFuzzySearch = rawQuery.length >= 3;
|
||||
|
||||
if (searchTerms.length === 0 && rawQuery.length < 3) {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const pageMatches = new Map();
|
||||
|
||||
// Pre-compute lower-case strings for each document
|
||||
const processedDocs = documents.map((doc, docId) => {
|
||||
const title = typeof doc.title === "string" ? doc.title : "";
|
||||
const content = typeof doc.content === "string" ? doc.content : "";
|
||||
|
||||
return {
|
||||
docId,
|
||||
doc,
|
||||
lowerTitle: title.toLowerCase(),
|
||||
lowerContent: content.toLowerCase(),
|
||||
};
|
||||
});
|
||||
|
||||
// First pass: Score pages with fuzzy matching
|
||||
processedDocs.forEach(({ docId, doc, lowerTitle, lowerContent }) => {
|
||||
let match = pageMatches.get(docId);
|
||||
if (!match) {
|
||||
match = { doc, pageScore: 0, matchingAnchors: [] };
|
||||
pageMatches.set(docId, match);
|
||||
}
|
||||
|
||||
if (useFuzzySearch) {
|
||||
const fuzzyTitleScore = fuzzyMatch(rawQuery, lowerTitle);
|
||||
if (fuzzyTitleScore !== null) {
|
||||
match.pageScore += fuzzyTitleScore * 100;
|
||||
}
|
||||
|
||||
const fuzzyContentScore = fuzzyMatch(rawQuery, lowerContent);
|
||||
if (fuzzyContentScore !== null) {
|
||||
match.pageScore += fuzzyContentScore * 30;
|
||||
}
|
||||
}
|
||||
|
||||
// Token-based exact matching
|
||||
searchTerms.forEach((term) => {
|
||||
if (lowerTitle.includes(term)) {
|
||||
match.pageScore += lowerTitle === term ? 20 : 10;
|
||||
}
|
||||
if (lowerContent.includes(term)) {
|
||||
match.pageScore += 2;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Second pass: Find matching anchors
|
||||
pageMatches.forEach((match) => {
|
||||
const doc = match.doc;
|
||||
if (
|
||||
!doc.anchors ||
|
||||
!Array.isArray(doc.anchors) ||
|
||||
doc.anchors.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.anchors.forEach((anchor) => {
|
||||
if (!anchor || !anchor.text) return;
|
||||
|
||||
const anchorText = anchor.text.toLowerCase();
|
||||
let anchorMatches = false;
|
||||
|
||||
if (useFuzzySearch) {
|
||||
const fuzzyScore = fuzzyMatch(rawQuery, anchorText);
|
||||
if (fuzzyScore !== null && fuzzyScore >= 0.4) {
|
||||
anchorMatches = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anchorMatches) {
|
||||
searchTerms.forEach((term) => {
|
||||
if (anchorText.includes(term)) {
|
||||
anchorMatches = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (anchorMatches) {
|
||||
match.matchingAnchors.push(anchor);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const results = Array.from(pageMatches.values())
|
||||
.filter((m) => m.pageScore > 5)
|
||||
.sort((a, b) => b.pageScore - a.pageScore)
|
||||
.slice(0, limit);
|
||||
|
||||
respond("results", results);
|
||||
}
|
||||
} catch (error) {
|
||||
respondError(error);
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -1,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 & 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 & Suggestions</h2>
|
||||
<p>Some quirks are not exactly quirks, but bugs in the module system. If you notice
|
||||
any issues with <strong>nvf</strong>, or this documentation, then please consider reporting
|
||||
them over at the <a href="https://github.com/notashelf/nvf/issues">issue tracker</a>. Issues tab, in addition to the
|
||||
<a href="https://github.com/notashelf/nvf/discussions">discussions tab</a> is a good place as any to request new features.</p>
|
||||
<p>You may also consider submitting bug fixes, feature additions and upstreamed
|
||||
changes that you think are critical over at the <a href="https://github.com/notashelf/nvf/pulls">pull requests tab</a>.</p>
|
||||
</body></html></main>
|
||||
</div>
|
||||
|
||||
<aside class="page-toc">
|
||||
<nav class="page-toc-nav">
|
||||
<h3>On this page</h3>
|
||||
<ul class="page-toc-list">
|
||||
<li><a href="#ch-known-issues-quirks">Known Issues and Quirks</a>
|
||||
<ul><li><a href="#ch-quirks-nodejs">NodeJS</a>
|
||||
<ul><li><a href="#sec-eslint-plugin-prettier">eslint-plugin-prettier</a>
|
||||
</ul><li><a href="#ch-bugs-suggestions">Bugs & Suggestions</a>
|
||||
</li></ul></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<footer>
|
||||
<p>Generated with ndg</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,110 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NVF - Search</title>
|
||||
|
||||
<script>
|
||||
// Apply sidebar state immediately to prevent flash
|
||||
(function () {
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/style.css" />
|
||||
<script defer src="assets/main.js"></script>
|
||||
<script>
|
||||
window.searchNamespace = window.searchNamespace || {};
|
||||
window.searchNamespace.rootPath = "";
|
||||
</script>
|
||||
<script defer src="assets/search.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-left">
|
||||
<h1 class="site-title">
|
||||
<a href="index.html">NVF</a>
|
||||
</h1>
|
||||
</div>
|
||||
<nav class="header-nav">
|
||||
<ul>
|
||||
<li >
|
||||
<a href="options.html">Options</a>
|
||||
</li>
|
||||
|
||||
<li><a href="search.html">Search</a></li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="search-container">
|
||||
<input type="text" id="search-input" placeholder="Search..." />
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<div class="sidebar-toggle" aria-label="Toggle sidebar">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<nav id="sidebar" class="sidebar">
|
||||
<div class="docs-nav">
|
||||
<h2>Documents</h2>
|
||||
<ul>
|
||||
<li><a href="index.html">Introduction</a></li>
|
||||
<li><a href="configuring.html">Configuring nvf</a></li>
|
||||
<li><a href="hacking.html">Hacking nvf</a></li>
|
||||
<li><a href="tips.html">Helpful Tips</a></li>
|
||||
<li><a href="quirks.html">Known Issues and Quirks</a></li>
|
||||
<li><a href="release-notes.html">Release Notes</a></li>
|
||||
<li><a href="search.html">Search</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="toc">
|
||||
<h2>Contents</h2>
|
||||
<ul class="toc-list">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
<h1>Search</h1>
|
||||
<div class="search-page">
|
||||
<div class="search-form">
|
||||
<input
|
||||
type="text"
|
||||
id="search-page-input"
|
||||
placeholder="Search..."
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<div id="search-page-results" class="search-page-results"></div>
|
||||
</div>
|
||||
<div class="footnotes-container">
|
||||
<!-- Footnotes will be appended here -->
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Generated with ndg</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
11
index.html
11
index.html
|
|
@ -5,12 +5,17 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Introduction</title>
|
||||
|
||||
|
||||
<script>
|
||||
// Apply sidebar state immediately to prevent flash
|
||||
(function () {
|
||||
try {
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
}
|
||||
} catch (e) {
|
||||
// localStorage unavailable
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/style.css" />
|
||||
|
|
@ -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
|
||||
|
|
|
|||
14573
options.html
14573
options.html
File diff suppressed because it is too large
Load diff
|
|
@ -5,12 +5,17 @@
|
|||
<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 () {
|
||||
try {
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
}
|
||||
} catch (e) {
|
||||
// localStorage unavailable
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/style.css" />
|
||||
|
|
|
|||
|
|
@ -5,12 +5,17 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Release Notes</title>
|
||||
|
||||
|
||||
<script>
|
||||
// Apply sidebar state immediately to prevent flash
|
||||
(function () {
|
||||
try {
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
}
|
||||
} catch (e) {
|
||||
// localStorage unavailable
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/style.css" />
|
||||
|
|
@ -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.<language></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> & {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> & <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>
|
||||
|
|
|
|||
38
search.html
38
search.html
|
|
@ -5,12 +5,17 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NVF - Search</title>
|
||||
|
||||
|
||||
<script>
|
||||
// Apply sidebar state immediately to prevent flash
|
||||
(function () {
|
||||
try {
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
}
|
||||
} catch (e) {
|
||||
// localStorage unavailable
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/style.css" />
|
||||
|
|
@ -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 -->
|
||||
|
|
|
|||
11
tips.html
11
tips.html
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue