mirror of
https://github.com/NotAShelf/nvf.git
synced 2026-04-27 03:47:37 +00:00
deploy: 35a64b0c64
This commit is contained in:
parent
0f6f61fa3d
commit
d39bd7cecd
64 changed files with 8748 additions and 249256 deletions
518
assets/main.js
518
assets/main.js
|
|
@ -61,13 +61,16 @@ function createMobileElements() {
|
||||||
const mobileSearchPopup = document.createElement("div");
|
const mobileSearchPopup = document.createElement("div");
|
||||||
mobileSearchPopup.id = "mobile-search-popup";
|
mobileSearchPopup.id = "mobile-search-popup";
|
||||||
mobileSearchPopup.className = "mobile-search-popup";
|
mobileSearchPopup.className = "mobile-search-popup";
|
||||||
|
mobileSearchPopup.setAttribute("role", "dialog");
|
||||||
|
mobileSearchPopup.setAttribute("aria-modal", "true");
|
||||||
|
mobileSearchPopup.setAttribute("aria-label", "Search");
|
||||||
mobileSearchPopup.innerHTML = `
|
mobileSearchPopup.innerHTML = `
|
||||||
<div class="mobile-search-container">
|
<div class="mobile-search-container" role="document">
|
||||||
<div class="mobile-search-header">
|
<div class="mobile-search-header">
|
||||||
<input type="text" id="mobile-search-input" placeholder="Search..." />
|
<input type="search" id="mobile-search-input" placeholder="Search..." aria-label="Search" autocomplete="off" />
|
||||||
<button id="close-mobile-search" class="close-mobile-search" aria-label="Close search">×</button>
|
<button type="button" id="close-mobile-search" class="close-mobile-search" aria-label="Close search">×</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -85,40 +88,51 @@ function createMobileElements() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize collapsible sidebar sections with state persistence
|
// Highlight search terms on target pages
|
||||||
function initCollapsibleSections() {
|
function highlightTextInContent(container, terms) {
|
||||||
// Target sections in both desktop and mobile sidebars
|
if (!container || !terms || terms.length === 0) return;
|
||||||
const sections = document.querySelectorAll(
|
|
||||||
".sidebar .sidebar-section, .mobile-sidebar-content .sidebar-section",
|
|
||||||
);
|
|
||||||
|
|
||||||
sections.forEach((section) => {
|
// Create a case-insensitive regex pattern
|
||||||
const sectionId = section.dataset.section;
|
const pattern = terms
|
||||||
if (!sectionId) return;
|
.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||||
|
.join("|");
|
||||||
|
const regex = new RegExp(`(${pattern})`, "gi");
|
||||||
|
|
||||||
const storageKey = `sidebar-section-${sectionId}`;
|
// Elements to skip highlighting
|
||||||
const savedState = localStorage.getItem(storageKey);
|
const skipTags = new Set(["SCRIPT", "STYLE", "CODE", "PRE", "MARK"]);
|
||||||
|
|
||||||
// Restore saved state (default is open)
|
function highlightNode(node) {
|
||||||
if (savedState === "closed") {
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
section.removeAttribute("open");
|
const text = node.textContent;
|
||||||
|
// Use match instead of test to avoid regex state issues
|
||||||
|
if (text.match(regex)) {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
// Create a fresh regex for replace to avoid state issues
|
||||||
|
const replaceRegex = new RegExp(`(${pattern})`, "gi");
|
||||||
|
span.innerHTML = text.replace(
|
||||||
|
replaceRegex,
|
||||||
|
'<mark class="search-highlight">$1</mark>',
|
||||||
|
);
|
||||||
|
node.replaceWith(...Array.from(span.childNodes));
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
node.nodeType === Node.ELEMENT_NODE &&
|
||||||
|
!skipTags.has(node.tagName)
|
||||||
|
) {
|
||||||
|
Array.from(node.childNodes).forEach(highlightNode);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save state on toggle and sync between desktop/mobile
|
highlightNode(container);
|
||||||
section.addEventListener("toggle", () => {
|
|
||||||
localStorage.setItem(storageKey, section.open ? "open" : "closed");
|
|
||||||
|
|
||||||
// Sync state between desktop and mobile versions
|
// Scroll to first highlight after a brief delay
|
||||||
const allWithSameSection = document.querySelectorAll(
|
setTimeout(() => {
|
||||||
`.sidebar-section[data-section="${sectionId}"]`,
|
const firstHighlight = container.querySelector(".search-highlight");
|
||||||
);
|
if (firstHighlight) {
|
||||||
allWithSameSection.forEach((el) => {
|
firstHighlight.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
if (el !== section) {
|
firstHighlight.classList.add("search-highlight-active");
|
||||||
el.open = section.open;
|
}
|
||||||
}
|
}, 100);
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize scroll spy
|
// Initialize scroll spy
|
||||||
|
|
@ -208,22 +222,100 @@ function initScrollSpy() {
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Apply sidebar state immediately before DOM rendering
|
// Apply sidebar state immediately before DOM rendering
|
||||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
try {
|
||||||
document.documentElement.classList.add("sidebar-collapsed");
|
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||||
document.body.classList.add("sidebar-collapsed");
|
document.documentElement.classList.add("sidebar-collapsed");
|
||||||
|
document.body.classList.add("sidebar-collapsed");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// localStorage unavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.querySelector(".mobile-sidebar-fab")) {
|
if (!document.querySelector(".mobile-sidebar-fab")) {
|
||||||
createMobileElements();
|
createMobileElements();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize collapsible sidebar sections
|
|
||||||
// after mobile elements are created
|
|
||||||
initCollapsibleSections();
|
|
||||||
|
|
||||||
// Initialize scroll spy for page TOC
|
// Initialize scroll spy for page TOC
|
||||||
initScrollSpy();
|
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
|
// Desktop Sidebar Toggle
|
||||||
const sidebarToggle = document.querySelector(".sidebar-toggle");
|
const sidebarToggle = document.querySelector(".sidebar-toggle");
|
||||||
|
|
||||||
|
|
@ -239,10 +331,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
document.body.classList.toggle("sidebar-collapsed");
|
document.body.classList.toggle("sidebar-collapsed");
|
||||||
|
|
||||||
// Use documentElement to check state and save to localStorage
|
// Use documentElement to check state and save to localStorage
|
||||||
const isCollapsed = document.documentElement.classList.contains(
|
const isCollapsed =
|
||||||
"sidebar-collapsed",
|
document.documentElement.classList.contains("sidebar-collapsed");
|
||||||
);
|
try {
|
||||||
localStorage.setItem("sidebar-collapsed", isCollapsed);
|
localStorage.setItem("sidebar-collapsed", isCollapsed);
|
||||||
|
} catch {
|
||||||
|
// localStorage unavailable
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -505,13 +600,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
const optionsContainer = document.querySelector(".options-container");
|
const optionsContainer = document.querySelector(".options-container");
|
||||||
if (!optionsContainer) return;
|
if (!optionsContainer) return;
|
||||||
|
|
||||||
// Only inject the style if it doesn't already exist
|
// Template container for hidden options
|
||||||
if (!document.head.querySelector("style[data-options-hidden]")) {
|
const hiddenOptionsContainer = document.createElement("template");
|
||||||
const styleEl = document.createElement("style");
|
hiddenOptionsContainer.id = "hidden-options-container";
|
||||||
styleEl.setAttribute("data-options-hidden", "");
|
document.body.appendChild(hiddenOptionsContainer);
|
||||||
styleEl.textContent = ".option-hidden{display:none!important}";
|
|
||||||
document.head.appendChild(styleEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create filter results counter
|
// Create filter results counter
|
||||||
const filterResults = document.createElement("div");
|
const filterResults = document.createElement("div");
|
||||||
|
|
@ -522,8 +614,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Detect if we're on a mobile device
|
// Detect if we're on a mobile device
|
||||||
const isMobile = window.innerWidth < 768 ||
|
const isMobile =
|
||||||
/Mobi|Android/i.test(navigator.userAgent);
|
window.innerWidth < 768 || /Mobi|Android/i.test(navigator.userAgent);
|
||||||
|
|
||||||
// Cache all option elements and their searchable content
|
// Cache all option elements and their searchable content
|
||||||
const options = Array.from(document.querySelectorAll(".option"));
|
const options = Array.from(document.querySelectorAll(".option"));
|
||||||
|
|
@ -580,29 +672,26 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
|
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
|
||||||
|
|
||||||
if (startIdx < itemsToProcess.length) {
|
if (startIdx < itemsToProcess.length) {
|
||||||
// Process current chunk
|
// Move visible items to container, hide others
|
||||||
for (let i = startIdx; i < endIdx; i++) {
|
for (let i = startIdx; i < endIdx; i++) {
|
||||||
const item = itemsToProcess[i];
|
const item = itemsToProcess[i];
|
||||||
if (item.visible) {
|
if (item.visible) {
|
||||||
item.element.classList.remove("option-hidden");
|
optionsContainer.appendChild(item.element);
|
||||||
} else {
|
} else {
|
||||||
item.element.classList.add("option-hidden");
|
hiddenOptionsContainer.content.appendChild(item.element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentChunk++;
|
currentChunk++;
|
||||||
pendingRender = requestAnimationFrame(processNextChunk);
|
pendingRender = requestAnimationFrame(processNextChunk);
|
||||||
} else {
|
} else {
|
||||||
// Finished processing all chunks
|
|
||||||
pendingRender = null;
|
pendingRender = null;
|
||||||
currentChunk = 0;
|
currentChunk = 0;
|
||||||
itemsToProcess = [];
|
itemsToProcess = [];
|
||||||
|
|
||||||
// Update counter at the very end for best performance
|
|
||||||
if (filterResults.visibleCount !== undefined) {
|
if (filterResults.visibleCount !== undefined) {
|
||||||
if (filterResults.visibleCount < totalCount) {
|
if (filterResults.visibleCount < totalCount) {
|
||||||
filterResults.textContent =
|
filterResults.textContent = `Showing ${filterResults.visibleCount} of ${totalCount} options`;
|
||||||
`Showing ${filterResults.visibleCount} of ${totalCount} options`;
|
|
||||||
filterResults.style.display = "block";
|
filterResults.style.display = "block";
|
||||||
} else {
|
} else {
|
||||||
filterResults.style.display = "none";
|
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() {
|
function filterOptions() {
|
||||||
const searchTerm = optionsFilter.value.toLowerCase().trim();
|
const searchTerm = optionsFilter.value.toLowerCase().trim();
|
||||||
|
|
||||||
|
// Skip if search term hasn't changed
|
||||||
|
if (filterOptions.lastTerm === searchTerm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filterOptions.lastTerm = searchTerm;
|
||||||
|
|
||||||
if (pendingRender) {
|
if (pendingRender) {
|
||||||
cancelAnimationFrame(pendingRender);
|
cancelAnimationFrame(pendingRender);
|
||||||
pendingRender = null;
|
pendingRender = null;
|
||||||
|
|
@ -622,12 +719,14 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
itemsToProcess = [];
|
itemsToProcess = [];
|
||||||
|
|
||||||
if (searchTerm === "") {
|
if (searchTerm === "") {
|
||||||
// Restore original DOM order when filter is cleared
|
// Restore to original order
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
originalOptionOrder.forEach((option) => {
|
originalOptionOrder.forEach((option) => {
|
||||||
option.classList.remove("option-hidden");
|
hiddenOptionsContainer.content.appendChild(option);
|
||||||
fragment.appendChild(option);
|
|
||||||
});
|
});
|
||||||
|
while (hiddenOptionsContainer.content.firstChild) {
|
||||||
|
fragment.appendChild(hiddenOptionsContainer.content.firstChild);
|
||||||
|
}
|
||||||
optionsContainer.appendChild(fragment);
|
optionsContainer.appendChild(fragment);
|
||||||
filterResults.style.display = "none";
|
filterResults.style.display = "none";
|
||||||
return;
|
return;
|
||||||
|
|
@ -640,55 +739,51 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
||||||
const titleMatches = [];
|
const titleMatches = [];
|
||||||
const descMatches = [];
|
const descMatches = [];
|
||||||
optionsData.forEach((data) => {
|
const term = searchTerms[0];
|
||||||
let isTitleMatch = false;
|
|
||||||
let isDescMatch = false;
|
for (let i = 0; i < optionsData.length; i++) {
|
||||||
if (searchTerms.length === 1) {
|
const data = optionsData[i];
|
||||||
const term = searchTerms[0];
|
const isTitleMatch = data.name.includes(term);
|
||||||
isTitleMatch = data.name.includes(term);
|
const isDescMatch = !isTitleMatch && data.description.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) {
|
if (isTitleMatch) {
|
||||||
|
visibleCount++;
|
||||||
titleMatches.push(data);
|
titleMatches.push(data);
|
||||||
} else if (isDescMatch) {
|
} else if (isDescMatch) {
|
||||||
|
visibleCount++;
|
||||||
descMatches.push(data);
|
descMatches.push(data);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (searchTerms.length === 1) {
|
|
||||||
const term = searchTerms[0];
|
|
||||||
titleMatches.sort(
|
|
||||||
(a, b) => a.name.indexOf(term) - b.name.indexOf(term),
|
|
||||||
);
|
|
||||||
descMatches.sort(
|
|
||||||
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
titleMatches.sort((a, b) => a.name.indexOf(term) - b.name.indexOf(term));
|
||||||
|
descMatches.sort(
|
||||||
|
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleElements = new Set();
|
||||||
itemsToProcess = [];
|
itemsToProcess = [];
|
||||||
titleMatches.forEach((data) => {
|
for (let i = 0; i < titleMatches.length; i++) {
|
||||||
visibleCount++;
|
const data = titleMatches[i];
|
||||||
|
visibleElements.add(data.element);
|
||||||
itemsToProcess.push({ element: data.element, visible: true });
|
itemsToProcess.push({ element: data.element, visible: true });
|
||||||
});
|
}
|
||||||
descMatches.forEach((data) => {
|
for (let i = 0; i < descMatches.length; i++) {
|
||||||
visibleCount++;
|
const data = descMatches[i];
|
||||||
|
visibleElements.add(data.element);
|
||||||
itemsToProcess.push({ element: data.element, visible: true });
|
itemsToProcess.push({ element: data.element, visible: true });
|
||||||
});
|
}
|
||||||
optionsData.forEach((data) => {
|
for (let i = 0; i < optionsData.length; i++) {
|
||||||
if (!itemsToProcess.some((item) => item.element === data.element)) {
|
const data = optionsData[i];
|
||||||
|
if (!visibleElements.has(data.element)) {
|
||||||
itemsToProcess.push({ element: data.element, visible: false });
|
itemsToProcess.push({ element: data.element, visible: false });
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Reorder DOM so all title matches, then desc matches, then hidden
|
// Reorder DOM so all title matches, then desc matches, then hidden
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
itemsToProcess.forEach((item) => {
|
for (let i = 0; i < itemsToProcess.length; i++) {
|
||||||
fragment.appendChild(item.element);
|
fragment.appendChild(itemsToProcess[i].element);
|
||||||
});
|
}
|
||||||
optionsContainer.appendChild(fragment);
|
optionsContainer.appendChild(fragment);
|
||||||
|
|
||||||
filterResults.visibleCount = visibleCount;
|
filterResults.visibleCount = visibleCount;
|
||||||
|
|
@ -700,7 +795,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
||||||
// Set up event listeners
|
// Set up event listeners
|
||||||
optionsFilter.addEventListener("input", debouncedFilter);
|
optionsFilter.addEventListener("input", debouncedFilter);
|
||||||
optionsFilter.addEventListener("change", filterOptions);
|
|
||||||
|
|
||||||
// Allow clearing with Escape key
|
// Allow clearing with Escape key
|
||||||
optionsFilter.addEventListener("keydown", function (e) {
|
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) {
|
if (optionsFilter.value) {
|
||||||
filterOptions();
|
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
504
assets/search.js
504
assets/search.js
|
|
@ -1,13 +1,25 @@
|
||||||
if (!window.searchNamespace) window.searchNamespace = {};
|
if (!window.searchNamespace) window.searchNamespace = {};
|
||||||
|
|
||||||
class SearchEngine {
|
class SearchEngine {
|
||||||
|
// Characters to strip from search term ends for better matching
|
||||||
|
static STRIP_TRAILING_CHARS_RE = /[.,!?;:'"…—–-]+$/g;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.documents = [];
|
this.documents = [];
|
||||||
this.tokenMap = new Map();
|
this.tokenMap = new Map();
|
||||||
|
this.lowercaseCache = [];
|
||||||
this.isLoaded = false;
|
this.isLoaded = false;
|
||||||
this.loadError = false;
|
this.loadError = false;
|
||||||
this.fullDocuments = null; // for lazy loading
|
this.fullDocuments = null; // for lazy loading
|
||||||
this.rootPath = window.searchNamespace?.rootPath || "";
|
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
|
// 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");
|
throw new Error("Search data file not found at any expected location");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Loading search data from: ${usedPath}`);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const documents = await response.json();
|
const documents = await response.json();
|
||||||
if (!Array.isArray(documents)) {
|
if (!Array.isArray(documents)) {
|
||||||
throw new Error("Invalid search data format");
|
// New format with config
|
||||||
|
if (documents.documents && Array.isArray(documents.documents)) {
|
||||||
|
this.config = {
|
||||||
|
minWordLength: documents.min_word_length || 2,
|
||||||
|
stopwords: documents.stopwords || [],
|
||||||
|
boostTitle: documents.boost_title || 100.0,
|
||||||
|
boostContent: documents.boost_content || 30.0,
|
||||||
|
boostAnchor: documents.boost_anchor || 10.0,
|
||||||
|
};
|
||||||
|
this.initializeFromDocuments(documents.documents);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid search data format");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy format - just an array of documents
|
||||||
|
this.initializeFromDocuments(documents);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initializeFromDocuments(documents);
|
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
console.log(`Loaded ${documents.length} documents for search`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading search data:", error);
|
console.error("Error loading search data:", error);
|
||||||
this.documents = [];
|
this.documents = [];
|
||||||
|
|
@ -81,7 +104,6 @@ class SearchEngine {
|
||||||
this.documents = [];
|
this.documents = [];
|
||||||
} else {
|
} else {
|
||||||
this.documents = documents;
|
this.documents = documents;
|
||||||
console.log(`Initialized with ${documents.length} documents`);
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.buildTokenMap();
|
await this.buildTokenMap();
|
||||||
|
|
@ -94,10 +116,13 @@ class SearchEngine {
|
||||||
initializeIndex(indexData) {
|
initializeIndex(indexData) {
|
||||||
this.documents = indexData.documents || [];
|
this.documents = indexData.documents || [];
|
||||||
this.tokenMap = new Map(Object.entries(indexData.tokenMap || {}));
|
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
|
// Build token map for faster searching
|
||||||
// This is helpful for faster searching with progressive loading
|
|
||||||
buildTokenMap() {
|
buildTokenMap() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.tokenMap.clear();
|
this.tokenMap.clear();
|
||||||
|
|
@ -111,6 +136,8 @@ class SearchEngine {
|
||||||
const totalDocs = this.documents.length;
|
const totalDocs = this.documents.length;
|
||||||
let processedDocs = 0;
|
let processedDocs = 0;
|
||||||
|
|
||||||
|
this.lowercaseCache = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Process in chunks to avoid blocking UI
|
// Process in chunks to avoid blocking UI
|
||||||
const processChunk = (startIndex, chunkSize) => {
|
const processChunk = (startIndex, chunkSize) => {
|
||||||
|
|
@ -128,7 +155,14 @@ class SearchEngine {
|
||||||
continue;
|
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) => {
|
tokens.forEach((token) => {
|
||||||
if (!this.tokenMap.has(token)) {
|
if (!this.tokenMap.has(token)) {
|
||||||
this.tokenMap.set(token, []);
|
this.tokenMap.set(token, []);
|
||||||
|
|
@ -143,9 +177,6 @@ class SearchEngine {
|
||||||
if (endIndex < totalDocs) {
|
if (endIndex < totalDocs) {
|
||||||
setTimeout(() => processChunk(endIndex, chunkSize), 0);
|
setTimeout(() => processChunk(endIndex, chunkSize), 0);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
|
||||||
`Built token map with ${this.tokenMap.size} unique tokens from ${processedDocs} documents`,
|
|
||||||
);
|
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -308,8 +339,8 @@ class SearchEngine {
|
||||||
}
|
}
|
||||||
score = Math.min(1.0, score + boundaryBonus);
|
score = Math.min(1.0, score + boundaryBonus);
|
||||||
|
|
||||||
const lengthPenalty = Math.abs(query.length - n) /
|
const lengthPenalty =
|
||||||
Math.max(query.length, m);
|
Math.abs(query.length - n) / Math.max(query.length, m);
|
||||||
score -= lengthPenalty * 0.2;
|
score -= lengthPenalty * 0.2;
|
||||||
|
|
||||||
return Math.max(0, Math.min(1.0, score));
|
return Math.max(0, Math.min(1.0, score));
|
||||||
|
|
@ -319,7 +350,13 @@ class SearchEngine {
|
||||||
if (!text || typeof text !== "string") return [];
|
if (!text || typeof text !== "string") return [];
|
||||||
|
|
||||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
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));
|
return Array.from(new Set(tokens));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -343,7 +380,6 @@ class SearchEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isLoaded || this.documents.length === 0) {
|
if (!this.isLoaded || this.documents.length === 0) {
|
||||||
console.log("Search data not available");
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,12 +393,23 @@ class SearchEngine {
|
||||||
|
|
||||||
const useFuzzySearch = rawQuery.length >= 3;
|
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 pageMatches = new Map();
|
||||||
const totalDocs = this.documents.length;
|
|
||||||
let lastCheckTime = Date.now();
|
let lastCheckTime = Date.now();
|
||||||
const CHECK_INTERVAL = 16; // Check every ~16ms (one frame)
|
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
|
// Check for abort periodically
|
||||||
if (Date.now() - lastCheckTime > CHECK_INTERVAL) {
|
if (Date.now() - lastCheckTime > CHECK_INTERVAL) {
|
||||||
if (options.signal?.aborted) {
|
if (options.signal?.aborted) {
|
||||||
|
|
@ -384,33 +431,37 @@ class SearchEngine {
|
||||||
pageMatches.set(docIdx, match);
|
pageMatches.set(docIdx, match);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lowerTitle = (
|
const cached = this.lowercaseCache?.[docIdx];
|
||||||
typeof doc.title === "string" ? doc.title : ""
|
const lowerTitle =
|
||||||
).toLowerCase();
|
cached?.title ??
|
||||||
const lowerContent = (
|
(typeof doc.title === "string" ? doc.title : "").toLowerCase();
|
||||||
typeof doc.content === "string" ? doc.content : ""
|
const lowerContent =
|
||||||
).toLowerCase();
|
cached?.content ??
|
||||||
|
(typeof doc.content === "string" ? doc.content : "").toLowerCase();
|
||||||
|
|
||||||
if (useFuzzySearch) {
|
if (useFuzzySearch) {
|
||||||
const fuzzyTitleScore = this.fuzzyMatch(rawQuery, lowerTitle);
|
const fuzzyTitleScore = this.fuzzyMatch(rawQuery, lowerTitle);
|
||||||
|
|
||||||
if (fuzzyTitleScore !== null) {
|
if (fuzzyTitleScore !== null) {
|
||||||
match.pageScore += fuzzyTitleScore * 100;
|
match.pageScore += fuzzyTitleScore * this.config.boostTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fuzzyContentScore = this.fuzzyMatch(rawQuery, lowerContent);
|
const fuzzyContentScore = this.fuzzyMatch(rawQuery, lowerContent);
|
||||||
|
|
||||||
if (fuzzyContentScore !== null) {
|
if (fuzzyContentScore !== null) {
|
||||||
match.pageScore += fuzzyContentScore * 30;
|
match.pageScore += fuzzyContentScore * this.config.boostContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchTerms.forEach((term) => {
|
searchTerms.forEach((term) => {
|
||||||
if (lowerTitle.includes(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)) {
|
if (lowerContent.includes(term)) {
|
||||||
match.pageScore += 2;
|
match.pageScore += this.config.boostContent / 15;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -629,9 +680,9 @@ class SearchEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const messageId = `search_${Date.now()}_${
|
const messageId = `search_${Date.now()}_${Math.random()
|
||||||
Math.random().toString(36).substring(2, 11)
|
.toString(36)
|
||||||
}`;
|
.substring(2, 11)}`;
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
reject(new Error("Web Worker search timeout"));
|
reject(new Error("Web Worker search timeout"));
|
||||||
|
|
@ -664,14 +715,12 @@ class SearchEngine {
|
||||||
worker.addEventListener("message", handleMessage);
|
worker.addEventListener("message", handleMessage);
|
||||||
worker.addEventListener("error", handleError);
|
worker.addEventListener("error", handleError);
|
||||||
|
|
||||||
worker.postMessage(
|
worker.postMessage({
|
||||||
{
|
messageId,
|
||||||
messageId,
|
type: "search",
|
||||||
type: "search",
|
data: { query, limit },
|
||||||
data: { query, limit },
|
documents: this.documents,
|
||||||
documents: this.documents,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -681,7 +730,7 @@ class SearchEngine {
|
||||||
return text
|
return text
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.replace(/[.,!?;:'"…—–-]+$/g, "")
|
.replace(SearchEngine.STRIP_TRAILING_CHARS_RE, "")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -829,13 +878,13 @@ class SearchEngine {
|
||||||
let pageScore = 0;
|
let pageScore = 0;
|
||||||
|
|
||||||
if (titleMatch !== -1) {
|
if (titleMatch !== -1) {
|
||||||
pageScore += 10;
|
pageScore += this.config.boostTitle / 10;
|
||||||
if (doc.title.toLowerCase() === lowerQuery) {
|
if (doc.title.toLowerCase() === lowerQuery) {
|
||||||
pageScore += 20;
|
pageScore += this.config.boostTitle / 5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (contentMatch !== -1) {
|
if (contentMatch !== -1) {
|
||||||
pageScore += 2;
|
pageScore += this.config.boostContent / 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find matching anchors
|
// Find matching anchors
|
||||||
|
|
@ -872,6 +921,105 @@ class SearchEngine {
|
||||||
// Create Web Worker if supported - initialized lazily to use rootPath
|
// Create Web Worker if supported - initialized lazily to use rootPath
|
||||||
let searchWorker = null;
|
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) {
|
function debounce(func, wait) {
|
||||||
let timeout = null;
|
let timeout = null;
|
||||||
return function (...args) {
|
return function (...args) {
|
||||||
|
|
@ -891,7 +1039,6 @@ function initializeSearchWorker() {
|
||||||
? `${rootPath}assets/search-worker.js`
|
? `${rootPath}assets/search-worker.js`
|
||||||
: "/assets/search-worker.js";
|
: "/assets/search-worker.js";
|
||||||
searchWorker = new Worker(workerPath);
|
searchWorker = new Worker(workerPath);
|
||||||
console.log("Web Worker initialized for background search");
|
|
||||||
return searchWorker;
|
return searchWorker;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Web Worker creation failed, using main thread:", error);
|
console.warn("Web Worker creation failed, using main thread:", error);
|
||||||
|
|
@ -913,9 +1060,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Initialize search engine immediately
|
// Initialize search engine immediately
|
||||||
window.searchNamespace.engine
|
window.searchNamespace.engine
|
||||||
.loadData()
|
.loadData()
|
||||||
.then(() => {
|
.then(() => {})
|
||||||
console.log("Search data loaded successfully");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Failed to initialize search:", error);
|
console.error("Failed to initialize search:", error);
|
||||||
});
|
});
|
||||||
|
|
@ -923,13 +1068,53 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Search page specific functionality
|
// Search page specific functionality
|
||||||
const searchPageInput = document.getElementById("search-page-input");
|
const searchPageInput = document.getElementById("search-page-input");
|
||||||
if (searchPageInput) {
|
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
|
// Set up event listener with debouncing
|
||||||
searchPageInput.addEventListener(
|
searchPageInput.addEventListener(
|
||||||
"input",
|
"input",
|
||||||
debounce(function () {
|
debounce(function () {
|
||||||
const query = this.value.trim();
|
const query = this.value.trim();
|
||||||
if (query.length >= 2) {
|
if (query.length >= 2) {
|
||||||
performSearch(query);
|
performSearch(query, searchPageKeyboardNav);
|
||||||
} else {
|
} else {
|
||||||
const resultsContainer = document.getElementById(
|
const resultsContainer = document.getElementById(
|
||||||
"search-page-results",
|
"search-page-results",
|
||||||
|
|
@ -938,6 +1123,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
resultsContainer.innerHTML =
|
resultsContainer.innerHTML =
|
||||||
"<p>Please enter at least 2 characters to search</p>";
|
"<p>Please enter at least 2 characters to search</p>";
|
||||||
}
|
}
|
||||||
|
searchPageKeyboardNav.clear();
|
||||||
}
|
}
|
||||||
}, 200),
|
}, 200),
|
||||||
);
|
);
|
||||||
|
|
@ -947,7 +1133,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
const query = params.get("q");
|
const query = params.get("q");
|
||||||
if (query) {
|
if (query) {
|
||||||
searchPageInput.value = query;
|
searchPageInput.value = query;
|
||||||
performSearch(query);
|
performSearch(query, searchPageKeyboardNav);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -956,6 +1142,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
const searchResults = document.getElementById("search-results");
|
const searchResults = document.getElementById("search-results");
|
||||||
const searchContainer = searchInput.closest(".search-container");
|
const searchContainer = searchInput.closest(".search-container");
|
||||||
|
// Initialize keyboard navigation for desktop search
|
||||||
|
const desktopKeyboardNav = new SearchKeyboardNav(
|
||||||
|
searchResults,
|
||||||
|
".search-result-item",
|
||||||
|
);
|
||||||
|
|
||||||
searchInput.addEventListener(
|
searchInput.addEventListener(
|
||||||
"input",
|
"input",
|
||||||
|
|
@ -967,6 +1158,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
searchResults.innerHTML = "";
|
searchResults.innerHTML = "";
|
||||||
searchResults.style.display = "none";
|
searchResults.style.display = "none";
|
||||||
if (searchContainer) searchContainer.classList.remove("has-results");
|
if (searchContainer) searchContainer.classList.remove("has-results");
|
||||||
|
desktopKeyboardNav.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -987,11 +1179,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
searchResults.innerHTML = results
|
searchResults.innerHTML = results
|
||||||
.map((result) => {
|
.map((result) => {
|
||||||
const { doc, matchingAnchors } = result;
|
const { doc, matchingAnchors } = result;
|
||||||
const queryTerms = window.searchNamespace.engine.tokenize(
|
const queryTerms =
|
||||||
searchTerm,
|
window.searchNamespace.engine.tokenize(searchTerm);
|
||||||
);
|
const highlightedTitle =
|
||||||
const highlightedTitle = window.searchNamespace.engine
|
window.searchNamespace.engine.highlightTerms(
|
||||||
.highlightTerms(
|
|
||||||
doc.title,
|
doc.title,
|
||||||
queryTerms,
|
queryTerms,
|
||||||
);
|
);
|
||||||
|
|
@ -1008,20 +1199,20 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
if (matchingAnchors && matchingAnchors.length > 0) {
|
if (matchingAnchors && matchingAnchors.length > 0) {
|
||||||
matchingAnchors.forEach((anchor) => {
|
matchingAnchors.forEach((anchor) => {
|
||||||
// Skip anchors that duplicate the page title
|
// Skip anchors that duplicate the page title
|
||||||
const normalizedAnchor = window.searchNamespace.engine
|
const normalizedAnchor =
|
||||||
.normalizeForComparison(
|
window.searchNamespace.engine.normalizeForComparison(
|
||||||
anchor.text,
|
anchor.text,
|
||||||
);
|
);
|
||||||
const normalizedTitle = window.searchNamespace.engine
|
const normalizedTitle =
|
||||||
.normalizeForComparison(
|
window.searchNamespace.engine.normalizeForComparison(
|
||||||
doc.title,
|
doc.title,
|
||||||
);
|
);
|
||||||
if (normalizedAnchor === normalizedTitle) {
|
if (normalizedAnchor === normalizedTitle) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlightedAnchor = window.searchNamespace.engine
|
const highlightedAnchor =
|
||||||
.highlightTerms(
|
window.searchNamespace.engine.highlightTerms(
|
||||||
anchor.text,
|
anchor.text,
|
||||||
queryTerms,
|
queryTerms,
|
||||||
);
|
);
|
||||||
|
|
@ -1039,6 +1230,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
.join("");
|
.join("");
|
||||||
searchResults.style.display = "block";
|
searchResults.style.display = "block";
|
||||||
if (searchContainer) searchContainer.classList.add("has-results");
|
if (searchContainer) searchContainer.classList.add("has-results");
|
||||||
|
desktopKeyboardNav.updateItems();
|
||||||
} else {
|
} else {
|
||||||
searchResults.innerHTML =
|
searchResults.innerHTML =
|
||||||
'<div class="search-result-item">No results found</div>';
|
'<div class="search-result-item">No results found</div>';
|
||||||
|
|
@ -1048,7 +1240,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Search error:", error);
|
console.error("Search error:", error);
|
||||||
searchResults.innerHTML =
|
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";
|
searchResults.style.display = "block";
|
||||||
if (searchContainer) searchContainer.classList.add("has-results");
|
if (searchContainer) searchContainer.classList.add("has-results");
|
||||||
}
|
}
|
||||||
|
|
@ -1063,6 +1255,35 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
) {
|
) {
|
||||||
searchResults.style.display = "none";
|
searchResults.style.display = "none";
|
||||||
if (searchContainer) searchContainer.classList.remove("has-results");
|
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();
|
event.preventDefault();
|
||||||
searchInput.focus();
|
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);
|
setupDocumentEventHandlers(searchInput, searchResults, searchContainer);
|
||||||
|
|
@ -1094,8 +1304,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
searchContainer,
|
searchContainer,
|
||||||
) {
|
) {
|
||||||
document.addEventListener("click", function (event) {
|
document.addEventListener("click", function (event) {
|
||||||
const isMobileSearchActive = mobileSearchPopup &&
|
const isMobileSearchActive =
|
||||||
mobileSearchPopup.classList.contains("active");
|
mobileSearchPopup && mobileSearchPopup.classList.contains("active");
|
||||||
const isDesktopResultsVisible = searchResults.style.display === "block";
|
const isDesktopResultsVisible = searchResults.style.display === "block";
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -1174,6 +1384,76 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
const mobileSearchResults = document.getElementById("mobile-search-results");
|
const mobileSearchResults = document.getElementById("mobile-search-results");
|
||||||
const closeMobileSearchBtn = document.getElementById("close-mobile-search");
|
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() {
|
function openMobileSearch() {
|
||||||
if (mobileSearchPopup) {
|
if (mobileSearchPopup) {
|
||||||
mobileSearchPopup.classList.add("active");
|
mobileSearchPopup.classList.add("active");
|
||||||
|
|
@ -1182,12 +1462,23 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
if (mobileSearchInput) {
|
if (mobileSearchInput) {
|
||||||
mobileSearchInput.focus();
|
mobileSearchInput.focus();
|
||||||
}
|
}
|
||||||
|
// Clean up previous session's listeners before setting up new ones
|
||||||
|
if (mobileFocusTrapCleanup) {
|
||||||
|
mobileFocusTrapCleanup();
|
||||||
|
mobileFocusTrapCleanup = null;
|
||||||
|
}
|
||||||
|
mobileFocusTrapCleanup = setupMobileFocusTrap();
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeMobileSearch() {
|
function closeMobileSearch() {
|
||||||
if (mobileSearchPopup) {
|
if (mobileSearchPopup) {
|
||||||
|
// Clean up event listeners before closing
|
||||||
|
if (mobileFocusTrapCleanup) {
|
||||||
|
mobileFocusTrapCleanup();
|
||||||
|
mobileFocusTrapCleanup = null;
|
||||||
|
}
|
||||||
mobileSearchPopup.classList.remove("active");
|
mobileSearchPopup.classList.remove("active");
|
||||||
if (mobileSearchInput) {
|
if (mobileSearchInput) {
|
||||||
mobileSearchInput.value = "";
|
mobileSearchInput.value = "";
|
||||||
|
|
@ -1235,11 +1526,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
mobileSearchResults.innerHTML = results
|
mobileSearchResults.innerHTML = results
|
||||||
.map((result) => {
|
.map((result) => {
|
||||||
const { doc, matchingAnchors } = result;
|
const { doc, matchingAnchors } = result;
|
||||||
const queryTerms = window.searchNamespace.engine.tokenize(
|
const queryTerms =
|
||||||
searchTerm,
|
window.searchNamespace.engine.tokenize(searchTerm);
|
||||||
);
|
const highlightedTitle =
|
||||||
const highlightedTitle = window.searchNamespace.engine
|
window.searchNamespace.engine.highlightTerms(
|
||||||
.highlightTerms(
|
|
||||||
doc.title,
|
doc.title,
|
||||||
queryTerms,
|
queryTerms,
|
||||||
);
|
);
|
||||||
|
|
@ -1258,25 +1548,25 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
if (matchingAnchors && matchingAnchors.length > 0) {
|
if (matchingAnchors && matchingAnchors.length > 0) {
|
||||||
matchingAnchors.forEach((anchor) => {
|
matchingAnchors.forEach((anchor) => {
|
||||||
// Skip anchors that duplicate the page title
|
// Skip anchors that duplicate the page title
|
||||||
const normalizedAnchor = window.searchNamespace.engine
|
const normalizedAnchor =
|
||||||
.normalizeForComparison(
|
window.searchNamespace.engine.normalizeForComparison(
|
||||||
anchor.text,
|
anchor.text,
|
||||||
);
|
);
|
||||||
const normalizedTitle = window.searchNamespace.engine
|
const normalizedTitle =
|
||||||
.normalizeForComparison(
|
window.searchNamespace.engine.normalizeForComparison(
|
||||||
doc.title,
|
doc.title,
|
||||||
);
|
);
|
||||||
if (normalizedAnchor === normalizedTitle) {
|
if (normalizedAnchor === normalizedTitle) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlightedAnchor = window.searchNamespace.engine
|
const highlightedAnchor =
|
||||||
.highlightTerms(
|
window.searchNamespace.engine.highlightTerms(
|
||||||
anchor.text,
|
anchor.text,
|
||||||
queryTerms,
|
queryTerms,
|
||||||
);
|
);
|
||||||
const sectionPreview = window.searchNamespace.engine
|
const sectionPreview =
|
||||||
.generateSectionPreview(
|
window.searchNamespace.engine.generateSectionPreview(
|
||||||
doc,
|
doc,
|
||||||
anchor,
|
anchor,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
|
|
@ -1298,6 +1588,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
mobileSearchResults.style.display = "block";
|
mobileSearchResults.style.display = "block";
|
||||||
|
// Clean up previous listeners before setting up new ones
|
||||||
|
if (mobileFocusTrapCleanup) {
|
||||||
|
mobileFocusTrapCleanup();
|
||||||
|
mobileFocusTrapCleanup = null;
|
||||||
|
}
|
||||||
|
mobileFocusTrapCleanup = setupMobileFocusTrap();
|
||||||
} else {
|
} else {
|
||||||
mobileSearchResults.innerHTML =
|
mobileSearchResults.innerHTML =
|
||||||
'<div class="search-result-item">No results found</div>';
|
'<div class="search-result-item">No results found</div>';
|
||||||
|
|
@ -1308,7 +1604,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Verify once more
|
// Verify once more
|
||||||
if (mobileSearchInput.value.trim() !== searchTerm) return;
|
if (mobileSearchInput.value.trim() !== searchTerm) return;
|
||||||
mobileSearchResults.innerHTML =
|
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";
|
mobileSearchResults.style.display = "block";
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
@ -1330,13 +1626,14 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function performSearch(query) {
|
async function performSearch(query, keyboardNav = null) {
|
||||||
query = query.trim();
|
query = query.trim();
|
||||||
const resultsContainer = document.getElementById("search-page-results");
|
const resultsContainer = document.getElementById("search-page-results");
|
||||||
|
|
||||||
if (query.length < 2) {
|
if (query.length < 2) {
|
||||||
resultsContainer.innerHTML =
|
resultsContainer.innerHTML =
|
||||||
"<p>Please enter at least 2 characters to search</p>";
|
"<p>Please enter at least 2 characters to search</p>";
|
||||||
|
if (keyboardNav) keyboardNav.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1348,6 +1645,7 @@ async function performSearch(query) {
|
||||||
|
|
||||||
// Show loading state
|
// Show loading state
|
||||||
resultsContainer.innerHTML = "<p>Searching...</p>";
|
resultsContainer.innerHTML = "<p>Searching...</p>";
|
||||||
|
if (keyboardNav) keyboardNav.clear();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await window.searchNamespace.engine.search(query, 50, {
|
const results = await window.searchNamespace.engine.search(query, 50, {
|
||||||
|
|
@ -1390,21 +1688,21 @@ async function performSearch(query) {
|
||||||
if (matchingAnchors && matchingAnchors.length > 0) {
|
if (matchingAnchors && matchingAnchors.length > 0) {
|
||||||
matchingAnchors.forEach((anchor) => {
|
matchingAnchors.forEach((anchor) => {
|
||||||
// Skip anchors that have the same text as the page title to avoid duplication
|
// Skip anchors that have the same text as the page title to avoid duplication
|
||||||
const normalizedAnchor = window.searchNamespace.engine
|
const normalizedAnchor =
|
||||||
.normalizeForComparison(anchor.text);
|
window.searchNamespace.engine.normalizeForComparison(anchor.text);
|
||||||
const normalizedTitle = window.searchNamespace.engine
|
const normalizedTitle =
|
||||||
.normalizeForComparison(doc.title);
|
window.searchNamespace.engine.normalizeForComparison(doc.title);
|
||||||
if (normalizedAnchor === normalizedTitle) {
|
if (normalizedAnchor === normalizedTitle) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlightedAnchor = window.searchNamespace.engine
|
const highlightedAnchor =
|
||||||
.highlightTerms(
|
window.searchNamespace.engine.highlightTerms(
|
||||||
anchor.text,
|
anchor.text,
|
||||||
queryTerms,
|
queryTerms,
|
||||||
);
|
);
|
||||||
const sectionPreview = window.searchNamespace.engine
|
const sectionPreview =
|
||||||
.generateSectionPreview(
|
window.searchNamespace.engine.generateSectionPreview(
|
||||||
doc,
|
doc,
|
||||||
anchor,
|
anchor,
|
||||||
query,
|
query,
|
||||||
|
|
@ -1421,8 +1719,10 @@ async function performSearch(query) {
|
||||||
}
|
}
|
||||||
html += "</ul>";
|
html += "</ul>";
|
||||||
resultsContainer.innerHTML = html;
|
resultsContainer.innerHTML = html;
|
||||||
|
if (keyboardNav) keyboardNav.updateItems();
|
||||||
} else {
|
} else {
|
||||||
resultsContainer.innerHTML = "<p>No results found</p>";
|
resultsContainer.innerHTML = "<p>No results found</p>";
|
||||||
|
if (keyboardNav) keyboardNav.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update URL with query
|
// Update URL with query
|
||||||
|
|
@ -1434,6 +1734,14 @@ async function performSearch(query) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.error("Search error:", error);
|
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);
|
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-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
--transition: 250ms 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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
@ -631,7 +634,7 @@ a:hover {
|
||||||
|
|
||||||
/* Code Styling */
|
/* Code Styling */
|
||||||
code {
|
code {
|
||||||
font-family: "JetBrains Mono", monospace;
|
font-family: var(--font-mono);
|
||||||
background-color: var(--code-bg);
|
background-color: var(--code-bg);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
|
|
@ -806,6 +809,9 @@ img {
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
transition: border-color var(--transition);
|
transition: border-color var(--transition);
|
||||||
|
min-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-result-item:last-child {
|
.search-result-item:last-child {
|
||||||
|
|
@ -816,12 +822,18 @@ img {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: block;
|
display: block;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-result-item:hover {
|
.search-result-item:hover {
|
||||||
background-color: var(--sidebar-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 {
|
.search-result-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
|
|
@ -879,7 +891,8 @@ img {
|
||||||
|
|
||||||
#mobile-search-input {
|
#mobile-search-input {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 10px 15px;
|
padding: var(--space-3) var(--space-4);
|
||||||
|
min-height: 48px;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
@ -899,9 +912,14 @@ img {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 0.5rem;
|
padding: 0;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-mobile-search:hover {
|
.close-mobile-search:hover {
|
||||||
|
|
@ -915,7 +933,10 @@ img {
|
||||||
|
|
||||||
/* Reuse desktop search result styling */
|
/* Reuse desktop search result styling */
|
||||||
.mobile-search-results .search-result-item {
|
.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);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -926,13 +947,24 @@ img {
|
||||||
.mobile-search-results .search-result-item a {
|
.mobile-search-results .search-result-item a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
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 {
|
.mobile-search-results .search-result-item:hover {
|
||||||
background-color: var(--sidebar-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 {
|
.mobile-search-results .search-result-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--heading-color);
|
color: var(--heading-color);
|
||||||
|
|
@ -981,6 +1013,50 @@ img {
|
||||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
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 {
|
.search-page-results {
|
||||||
margin-top: var(--space-6);
|
margin-top: var(--space-6);
|
||||||
}
|
}
|
||||||
|
|
@ -1658,7 +1734,7 @@ h6:hover .copy-link {
|
||||||
.toc-list details summary {
|
.toc-list details summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-3);
|
||||||
font-weight: 500;
|
font-weight: 400;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1683,11 +1759,10 @@ h6:hover .copy-link {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc-list details summary::after {
|
.toc-list details summary::before {
|
||||||
content: "▶";
|
content: "▶";
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
margin-left: auto;
|
margin-right: var(--space-2);
|
||||||
margin-right: var(--space-1);
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
transition:
|
transition:
|
||||||
|
|
@ -1695,7 +1770,7 @@ h6:hover .copy-link {
|
||||||
color var(--transition-fast);
|
color var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc-list details[open] summary::after {
|
.toc-list details[open] summary::before {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
}
|
}
|
||||||
|
|
@ -1713,7 +1788,7 @@ h6:hover .copy-link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: var(--space-2);
|
margin-left: auto;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1800,6 +1875,151 @@ h6:hover .copy-link {
|
||||||
margin-top: var(--space-3);
|
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 */
|
/* Filter styling */
|
||||||
.search-form {
|
.search-form {
|
||||||
margin: var(--space-4) 0;
|
margin: var(--space-4) 0;
|
||||||
|
|
@ -1808,6 +2028,7 @@ h6:hover .copy-link {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#lib-filter,
|
||||||
#options-filter {
|
#options-filter {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
|
|
@ -1820,6 +2041,7 @@ h6:hover .copy-link {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#lib-filter:focus,
|
||||||
#options-filter:focus {
|
#options-filter:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--link-color);
|
border-color: var(--link-color);
|
||||||
|
|
@ -2005,7 +2227,8 @@ h6:hover .copy-link {
|
||||||
margin-bottom: var(--space-2);
|
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;
|
font-size: 0.875rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
|
@ -2023,24 +2246,24 @@ h6:hover .copy-link {
|
||||||
background-color var(--transition-fast);
|
background-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section summary:hover {
|
.sidebar-section > summary:hover {
|
||||||
background-color: var(--sidebar-hover);
|
background-color: var(--sidebar-hover);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section summary::before {
|
.sidebar-section > summary::before {
|
||||||
content: "▶";
|
content: "▶";
|
||||||
font-size: 0.625rem;
|
font-size: 0.625rem;
|
||||||
transition: transform var(--transition-fast);
|
transition: transform var(--transition-fast);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section[open] summary::before {
|
.sidebar-section[open] > summary::before {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide default marker */
|
/* Hide default marker */
|
||||||
.sidebar-section summary::-webkit-details-marker {
|
.sidebar-section > summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2141,3 +2364,176 @@ h6:hover .copy-link {
|
||||||
display: none !important;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
167
configuring.html
167
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
13
hacking.html
13
hacking.html
File diff suppressed because one or more lines are too long
15
index.html
15
index.html
|
|
@ -5,11 +5,16 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Introduction</title>
|
<title>Introduction</title>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Apply sidebar state immediately to prevent flash
|
// Apply sidebar state immediately to prevent flash
|
||||||
(function () {
|
(function () {
|
||||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
try {
|
||||||
document.documentElement.classList.add("sidebar-collapsed");
|
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||||
|
document.documentElement.classList.add("sidebar-collapsed");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// localStorage unavailable
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -111,7 +116,7 @@
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="content"><html><head></head><body><h1 id="nvf-manual">Introduction</h1>
|
<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>
|
<h2 id="ch-preface">Preface</h2>
|
||||||
<h3 id="sec-what-is-it">What is nvf</h3>
|
<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
|
<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 class="admonition-title">Note</p>
|
||||||
<p><strong>nvf</strong> exposes a lot of options, most of which are not referenced in the
|
<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
|
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>
|
</div>
|
||||||
<h2 id="sec-nixos-flakeless">Without Flakes</h2>
|
<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
|
<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 class="admonition-title">Note</p>
|
||||||
<p><strong>nvf</strong> exposes a lot of options, most of which are not referenced in the
|
<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
|
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>
|
</div>
|
||||||
<h2 id="sec-hm-flakeless">Without Flakes</h2>
|
<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
|
<p>As of v0.8, it is possible to install <strong>nvf</strong> on a system if you are not using
|
||||||
|
|
|
||||||
14577
options.html
14577
options.html
File diff suppressed because it is too large
Load diff
|
|
@ -5,11 +5,16 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Known Issues and Quirks</title>
|
<title>Known Issues and Quirks</title>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Apply sidebar state immediately to prevent flash
|
// Apply sidebar state immediately to prevent flash
|
||||||
(function () {
|
(function () {
|
||||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
try {
|
||||||
document.documentElement.classList.add("sidebar-collapsed");
|
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||||
|
document.documentElement.classList.add("sidebar-collapsed");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// localStorage unavailable
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,16 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Release Notes</title>
|
<title>Release Notes</title>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Apply sidebar state immediately to prevent flash
|
// Apply sidebar state immediately to prevent flash
|
||||||
(function () {
|
(function () {
|
||||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
try {
|
||||||
document.documentElement.classList.add("sidebar-collapsed");
|
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||||
|
document.documentElement.classList.add("sidebar-collapsed");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// localStorage unavailable
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -138,7 +143,7 @@ setting. Use <code>workspaces</code> instead:</p>
|
||||||
<p>Some other settings and commands are now deprecated but are still supported.</p>
|
<p>Some other settings and commands are now deprecated but are still supported.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>The <code>setupOpts.mappings</code> options were also removed. Use the built-in Neovim
|
<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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
<p>Renamed <code>languages.ts</code> to <code>languages.typescript</code>.</p>
|
||||||
</li>
|
</li>
|
||||||
<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
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2 id="sec-release-0-9-changelog">Changelog</h2>
|
<h2 id="sec-release-0-9-changelog">Changelog</h2>
|
||||||
|
|
@ -310,7 +315,7 @@ values in <code>vim.treesitter.grammars</code>.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p><a href="https://github.com/jfeo">jfeo</a>:</p>
|
<p><a href="https://github.com/jfeo">jfeo</a>:</p>
|
||||||
<ul>
|
<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>
|
hard-coded options as default values.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p><a href="https://github.com/Ring-A-Ding-Ding-Baby">Ring-A-Ding-Ding-Baby</a>:</p>
|
<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>
|
support to <code>languages.python</code></p>
|
||||||
</li>
|
</li>
|
||||||
<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
|
<a href="https://tombi-toml.github.io/tombi/">Tombi</a> language server, linter, and
|
||||||
formatter.</p>
|
formatter.</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -460,14 +465,14 @@ formatter.</p>
|
||||||
<p><a href="https://github.com/snoweuph">Snoweuph</a></p>
|
<p><a href="https://github.com/snoweuph">Snoweuph</a></p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<p>Added injections for <code>vim.treesitter.queries.*.content</code> as <code>query</code> and
|
<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>
|
<code>mkLualine</code>, <code>entryAnywhere</code>, <code>entryBefore</code>, <code>entryAfter</code> as <code>lua</code> in nix.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
language the content is.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
<code>deprecatedSingleOrListOf</code> in favor of <code>listOf</code> for the affected LSP options.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
Template support.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -488,7 +493,7 @@ Template support.</p>
|
||||||
out.</p>
|
out.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
tree-sitter incompatibilities.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -605,11 +610,11 @@ previewing yet.</p>
|
||||||
<p>Extend <code>languages.asm</code> to support more filetypes out of the box.</p>
|
<p>Extend <code>languages.asm</code> to support more filetypes out of the box.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
support;</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
support;</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -725,7 +730,7 @@ upstream.</p>
|
||||||
<li>
|
<li>
|
||||||
<p><code>vim.useSystemClipboard</code> has been deprecated as a part of removing most
|
<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
|
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>
|
to configure Neovim to use the system clipboard.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -754,18 +759,18 @@ can remove them now.</p>
|
||||||
<code>languages.markdown.extensions.render-markdown-nvim</code>.</p>
|
<code>languages.markdown.extensions.render-markdown-nvim</code>.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
in gitsigns configuration.</p>
|
||||||
</li>
|
</li>
|
||||||
<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
|
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
|
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>
|
checked, so you will be responsible for ensuring its validity.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>Deprecate <code>vim.enableEditorconfig</code> in favor of
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<p>Deprecate rnix-lsp as it has been abandoned and archived upstream.</p>
|
<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>
|
values for buffers with the Nix filetype.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
previously managed lightbulb autocommand.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>A warning will occur if {option} vim-lsp-lightbulb-autocmd-enable) and
|
<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>
|
<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>
|
||||||
<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>
|
autocommands via Nix.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -828,7 +833,7 @@ integration for blink-cmp and nvim-cmp</p>
|
||||||
<li>
|
<li>
|
||||||
<p>Add <code>vim.diagnostics</code> to interact with Neovim's diagnostics module. Available
|
<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
|
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>
|
||||||
<li>
|
<li>
|
||||||
<p>Add <code>vim.clipboard</code> module for easily managing Neovim clipboard providers and
|
<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/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>
|
<li>Add <a href="https://github.com/HiPhish/rainbow-delimiters.nvim">rainbow-delimiters</a>
|
||||||
in <code>vim.visuals.rainbow-delimiters</code></li>
|
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>
|
</ul>
|
||||||
<p><a href="https://github.com/kaktu5">kaktu5</a>:</p>
|
<p><a href="https://github.com/kaktu5">kaktu5</a>:</p>
|
||||||
<ul>
|
<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>
|
options that were under <code>vim</code> as convenient shorthands for <code>vim.o.*</code> options.</p>
|
||||||
<div class="admonition warning">
|
<div class="admonition warning">
|
||||||
<p class="admonition-title">Warning</p>
|
<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
|
considered as deprecated. You should migrate to the appropriate options in the
|
||||||
<code>vim.options</code> submodule.</p>
|
<code>vim.options</code> submodule.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1369,7 +1374,7 @@ considered as deprecated. You should migrate to the appropriate options in the
|
||||||
<li>
|
<li>
|
||||||
<p><code>colourTerm</code>, <code>mouseSupport</code>, <code>cmdHeight</code>, <code>updateTime</code>, <code>mapTime</code>,
|
<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
|
<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>
|
definition for the updated options.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -1388,7 +1393,7 @@ will enable the <code>typst-lsp</code> language server, and the <code>typstfmt</
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<p>Modified type for
|
<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>
|
<code>anything</code> to a <code>submodule</code> for better type checking.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
<code>NvimTreeNormal</code> to <code>none</code>.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
type <code>int</code> instead of the expected type <code>string</code>.</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -1495,19 +1500,19 @@ aren't defined in nvf. Move the alternate nvim-surround keybinds to use
|
||||||
<li>
|
<li>
|
||||||
<p>Remove <code>autopairs.type</code>, and rename <code>autopairs.enable</code> to
|
<p>Remove <code>autopairs.type</code>, and rename <code>autopairs.enable</code> to
|
||||||
<code>autopairs.nvim-autopairs.enable</code>. The new
|
<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>
|
default.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>Refactor of <code>nvim-cmp</code> and completion related modules</p>
|
<p>Refactor of <code>nvim-cmp</code> and completion related modules</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Remove <code>autocomplete.type</code> in favor of per-plugin enable options such as
|
<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
|
<li>Deprecate legacy Vimsnip in favor of Luasnip, and integrate
|
||||||
friendly-snippets for bundled snippets.
|
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
|
<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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -1591,11 +1596,11 @@ identical clone.</li>
|
||||||
Lualine. Only <code>vim.ui.breadcrumbs.lualine.winbar</code> is supported for the time
|
Lualine. Only <code>vim.ui.breadcrumbs.lualine.winbar</code> is supported for the time
|
||||||
being.</p>
|
being.</p>
|
||||||
<ul>
|
<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,
|
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>
|
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
|
<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>
|
the new format.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -1608,11 +1613,11 @@ server and make it default.</p>
|
||||||
additional Python LSP server.</p>
|
additional Python LSP server.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
without using additional Lua. See option documentation for more details.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
configuration for <a href="https://github.com/nvimdev/dashboard-nvim">dashboard.nvim</a></p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -1623,7 +1628,7 @@ configuration for <a href="https://github.com/nvimdev/dashboard-nvim">dashboard.
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
spellfiles to Neovim's runtime with ease.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -1687,9 +1692,9 @@ the Typst language module.</li>
|
||||||
<p><a href="https://github.com/nezia1">nezia1</a>:</p>
|
<p><a href="https://github.com/nezia1">nezia1</a>:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Add <a href="https://github.com/biomejs/biome">biome</a> support for Typescript, CSS and
|
<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>,
|
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.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>
|
<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
|
<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>
|
<a href="https://github.com/NixOS/nixfmt">nixfmt</a> (nixfmt-rfc-style).</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -1725,7 +1730,7 @@ lot):</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>Added <code>ChatGPT.nvim</code>, which can be enabled with
|
<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>
|
requires <code>OPENAI_API_KEY</code> environment variable to be set.</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -1787,7 +1792,7 @@ and also has been removed.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p><code>which-key.nvim</code> categories can now be customized through
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<p>Added <code>magick</code> to <code>vim.luaPackages</code> for <code>image.nvim</code>.</p>
|
<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>
|
<li>
|
||||||
<p>Lualine module now allows customizing <code>always_divide_middle</code>, <code>ignore_focus</code>
|
<p>Lualine module now allows customizing <code>always_divide_middle</code>, <code>ignore_focus</code>
|
||||||
and <code>disabled_filetypes</code> through the new options:
|
and <code>disabled_filetypes</code> through the new options:
|
||||||
<a href="./options.html#option-vim-statusline-lualine-alwaysDivideMiddle">vim.statusline.lualine.alwaysDivideMiddle</a>,
|
<a class="option-reference" href="options.html#option-vim.statusline.lualine.alwaysDivideMiddle"><code class="nixos-option">vim.statusline.lualine.alwaysDivideMiddle</code></a>,
|
||||||
<a href="./options.html#option-vim-statusline-lualine-ignoreFocus">vim.statusline.lualine.ignoreFocus</a>
|
<a class="option-reference" href="options.html#option-vim.statusline.lualine.ignoreFocus"><code class="nixos-option">vim.statusline.lualine.ignoreFocus</code></a> and
|
||||||
and
|
<a class="option-reference" href="options.html#option-vim.statusline.lualine.disabledFiletypes"><code class="nixos-option">vim.statusline.lualine.disabledFiletypes</code></a>).</p>
|
||||||
<a href="./options.html#option-vim-statusline-lualine-disabledFiletypes">vim.statusline.lualine.disabledFiletypes</a>.</p>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>Updated all plugin inputs to their latest versions (<strong>21.04.2024</strong>) - this
|
<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>
|
are then concatted inside a lua block.</p>
|
||||||
</li>
|
</li>
|
||||||
<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
|
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
|
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>
|
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>
|
the <code>makeNeovimConfig</code> function under their respective options.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
wrapper PATH, making said packages available while inside the Neovim session.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
<code>setupOpts</code> while it is enabled.</p>
|
||||||
</li>
|
</li>
|
||||||
<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
|
string of enum, or a Lua function. The default is "compact", but you may
|
||||||
change it according to nvim-notify documentation.</p>
|
change it according to nvim-notify documentation.</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -1920,7 +1924,7 @@ change it according to nvim-notify documentation.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>Streamlined and simplified extra plugin API with the addition of
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<p>Allow using command names in place of LSP packages to avoid automatic
|
<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>
|
<p>Add lua LSP and Treesitter support, and neodev.nvim plugin support</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p><a href="https://github.com/amanse">amanse</a>:</p>
|
<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>
|
<p>Added GitHub Copilot to nvim-cmp completion sources.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
configuration.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
nvim-navic</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -1990,7 +1994,7 @@ enabled if navic is enabled)</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>Added support for <code>statix</code> and <code>deadnix</code> through
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<p>Added <code>lsp_lines</code> plugin for showing diagnostic messages</p>
|
<p>Added <code>lsp_lines</code> plugin for showing diagnostic messages</p>
|
||||||
|
|
@ -2000,7 +2004,7 @@ enabled if navic is enabled)</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>The package used for neovim is now customizable by the user, using
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<p>Added highlight-undo plugin for highlighting undo/redo targets</p>
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<p>Treesitter grammars are now configurable with
|
<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
|
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
|
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.
|
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>
|
<li>
|
||||||
<p>A new section has been added for language support: <code>vim.languages.<language></code>.</p>
|
<p>A new section has been added for language support: <code>vim.languages.<language></code>.</p>
|
||||||
<ul>
|
<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>
|
enable the respective section for all languages that have been enabled.</li>
|
||||||
<li>All LSP languages have been moved here</li>
|
<li>All LSP languages have been moved here</li>
|
||||||
<li><code>plantuml</code> and <code>markdown</code> 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
|
<li>A new section has been added for <code>html</code>. The old
|
||||||
<code>vim.treesitter.autotagHtml</code> can be found at
|
<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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
<p>Removed the plugins document in the docs. Was too unwieldy to keep updated.</p>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
||||||
<li>
|
<li>
|
||||||
<p>Improved handling of completion formatting. When setting
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<p>Transparency has been made optional and has been disabled by default.
|
<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>
|
transparency for your configuration.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -2363,7 +2367,7 @@ longer defined. If you use hare and would like it added back, please file an
|
||||||
issue.</p>
|
issue.</p>
|
||||||
</li>
|
</li>
|
||||||
<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
|
<code>string</code> for options sourced from the flake inputs. Users can still provide
|
||||||
vim plugin packages.</p>
|
vim plugin packages.</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -2379,14 +2383,14 @@ longer required. See the manual for the new way to configuration.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<p>Treesitter grammars are now configurable with
|
<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
|
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.
|
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>
|
Packages can be found under the <code>vimPlugins.nvim-treesitter.builtGrammars</code>
|
||||||
namespace.</p>
|
namespace.</p>
|
||||||
</li>
|
</li>
|
||||||
<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
|
allows for ordering of the config. Usage is the same is in home-manager's
|
||||||
<code>home.activation</code> option.</p>
|
<code>home.activation</code> option.</p>
|
||||||
</li>
|
</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>
|
<p><a href="https://github.com/MoritzBoehme">MoritzBoehme</a>:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>catppuccin</code> theme is now available as a neovim theme
|
<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.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.statusline.lualine.theme"><code class="nixos-option">vim.statusline.lualine.theme</code></a>.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</body></html></main>
|
</body></html></main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
42
search.html
42
search.html
|
|
@ -5,11 +5,16 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>NVF - Search</title>
|
<title>NVF - Search</title>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Apply sidebar state immediately to prevent flash
|
// Apply sidebar state immediately to prevent flash
|
||||||
(function () {
|
(function () {
|
||||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
try {
|
||||||
document.documentElement.classList.add("sidebar-collapsed");
|
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||||
|
document.documentElement.classList.add("sidebar-collapsed");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// localStorage unavailable
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -41,8 +46,20 @@
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<input type="text" id="search-input" placeholder="Search..." />
|
<input
|
||||||
<div id="search-results" class="search-results"></div>
|
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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -85,13 +102,26 @@
|
||||||
<div class="search-page">
|
<div class="search-page">
|
||||||
<div class="search-form">
|
<div class="search-form">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="search"
|
||||||
id="search-page-input"
|
id="search-page-input"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
|
aria-label="Search"
|
||||||
|
autocomplete="off"
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
<div class="footnotes-container">
|
<div class="footnotes-container">
|
||||||
<!-- Footnotes will be appended here -->
|
<!-- Footnotes will be appended here -->
|
||||||
|
|
|
||||||
15
tips.html
15
tips.html
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue