Initalize

This commit is contained in:
Your Name
2026-05-03 12:12:57 -04:00
commit 38652eb9b5
10603 changed files with 1762136 additions and 0 deletions

View File

@@ -0,0 +1,169 @@
import { isObject } from '../../shared';
/**
* Resolves the effective mode based on config and available features.
*
* - 'auto': infer hybrid vs sidePanel-only from provided config
* - 'sidePanel': force sidePanel-only even if keyword search is configured
* - 'hybrid': force hybrid (error if keyword search is not configured)
* - 'modal': force modal even if sidePanel is configured
*/
export function resolveMode(options) {
const mode = options.mode ?? 'auto';
const hasKeyword = hasKeywordSearch(options);
const askAi = options.askAi;
const hasSidePanelConfig = Boolean(askAi && typeof askAi === 'object' && askAi.sidePanel);
switch (mode) {
case 'sidePanel':
// Force sidePanel-only - hide keyword search
return {
mode,
showKeywordSearch: false,
useSidePanel: true
};
case 'hybrid':
// Force hybrid - keyword search must be configured
if (!hasKeyword) {
console.error('[vitepress] mode: "hybrid" requires keyword search credentials (appId, apiKey, indexName).');
}
return {
mode,
showKeywordSearch: hasKeyword,
useSidePanel: true
};
case 'modal':
// Force modal - don't use sidepanel for askai, even if configured
return {
mode,
showKeywordSearch: hasKeyword,
useSidePanel: false
};
case 'auto':
default:
// Auto-detect based on config
return {
mode: 'auto',
showKeywordSearch: hasKeyword,
useSidePanel: hasSidePanelConfig
};
}
}
export function hasKeywordSearch(options) {
return Boolean(options.appId && options.apiKey && options.indexName);
}
export function hasAskAi(askAi) {
if (!askAi)
return false;
if (typeof askAi === 'string')
return askAi.length > 0;
return Boolean(askAi.assistantId);
}
/**
* Removes existing `lang:` filters and appends `lang:${lang}`.
* Handles both flat arrays and nested arrays (for OR conditions).
*/
export function mergeLangFacetFilters(rawFacetFilters, lang) {
const input = Array.isArray(rawFacetFilters)
? rawFacetFilters
: rawFacetFilters
? [rawFacetFilters]
: [];
const filtered = input
.map((filter) => {
if (Array.isArray(filter)) {
// Handle nested arrays (OR conditions)
return filter.filter((f) => typeof f === 'string' && !f.startsWith('lang:'));
}
return filter;
})
.filter((filter) => {
if (typeof filter === 'string') {
return !filter.startsWith('lang:');
}
// Keep nested arrays with remaining filters
return Array.isArray(filter) && filter.length > 0;
});
return [...filtered, `lang:${lang}`];
}
/**
* Validates that required Algolia credentials are present.
*/
export function validateCredentials(options) {
const appId = options.appId;
const apiKey = options.apiKey;
const indexName = options.indexName;
return {
valid: Boolean(appId && apiKey && indexName),
appId,
apiKey,
indexName
};
}
/**
* Builds Ask AI configuration from various input formats.
*/
export function buildAskAiConfig(askAiProp, options, lang) {
const isAskAiString = typeof askAiProp === 'string';
const askAiSearchParameters = !isAskAiString && askAiProp.searchParameters
? { ...askAiProp.searchParameters }
: undefined;
// If Ask AI defines its own facetFilters, merge lang filtering into those.
// Otherwise, reuse the keyword search facetFilters so Ask AI follows the
// same language filtering behavior by default.
const askAiFacetFiltersSource = askAiSearchParameters?.facetFilters ??
options.searchParameters?.facetFilters;
const askAiFacetFilters = mergeLangFacetFilters(askAiFacetFiltersSource, lang);
const mergedAskAiSearchParameters = {
...askAiSearchParameters,
facetFilters: askAiFacetFilters.length ? askAiFacetFilters : undefined
};
const result = {
...(isAskAiString ? {} : askAiProp),
indexName: isAskAiString ? options.indexName : askAiProp.indexName,
apiKey: isAskAiString ? options.apiKey : askAiProp.apiKey,
appId: isAskAiString ? options.appId : askAiProp.appId,
assistantId: isAskAiString ? askAiProp : askAiProp.assistantId
};
// Keep `searchParameters` undefined unless it has at least one key.
if (Object.values(mergedAskAiSearchParameters).some((v) => v != null)) {
result.searchParameters = mergedAskAiSearchParameters;
}
return result;
}
/**
* Resolves Algolia search options for the given language,
* merging in locale-specific overrides and language facet filters.
*/
export function resolveOptionsForLanguage(options, localeIndex, lang) {
options = deepMerge(options, options.locales?.[localeIndex] || {});
const facetFilters = mergeLangFacetFilters(options.searchParameters?.facetFilters, lang);
const askAi = options.askAi
? buildAskAiConfig(options.askAi, options, lang)
: undefined;
return {
...options,
searchParameters: { ...options.searchParameters, facetFilters },
askAi
};
}
function deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
const value = source[key];
if (value === undefined)
continue;
// special case: replace entirely
if (key === 'searchParameters') {
result[key] = value;
continue;
}
// deep-merge only plain objects; arrays are replaced entirely
if (isObject(value) && isObject(result[key])) {
result[key] = deepMerge(result[key], value);
}
else {
result[key] = value;
}
}
delete result.locales;
return result;
}

View File

@@ -0,0 +1,33 @@
// adapted from https://stackoverflow.com/a/46432113/11613622
export class LRUCache {
max;
cache;
constructor(max = 10) {
this.max = max;
this.cache = new Map();
}
get(key) {
let item = this.cache.get(key);
if (item !== undefined) {
// refresh key
this.cache.delete(key);
this.cache.set(key, item);
}
return item;
}
set(key, val) {
// refresh key
if (this.cache.has(key))
this.cache.delete(key);
// evict oldest
else if (this.cache.size === this.max)
this.cache.delete(this.first());
this.cache.set(key, val);
}
first() {
return this.cache.keys().next().value;
}
clear() {
this.cache.clear();
}
}

View File

@@ -0,0 +1,9 @@
import { computed } from 'vue';
export function smartComputed(getter, comparator = (oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)) {
return computed((oldValue) => {
const newValue = getter();
return oldValue === undefined || !comparator(oldValue, newValue)
? newValue
: oldValue;
});
}

View File

@@ -0,0 +1,89 @@
import { isActive } from '../../shared';
import { ensureStartingSlash } from './utils';
/**
* Get the `Sidebar` from sidebar option. This method will ensure to get correct
* sidebar config from `MultiSideBarConfig` with various path combinations such
* as matching `guide/` and `/guide/`. If no matching config was found, it will
* return empty array.
*/
export function getSidebar(_sidebar, path) {
if (Array.isArray(_sidebar))
return addBase(_sidebar);
if (_sidebar == null)
return [];
path = ensureStartingSlash(path);
const dir = Object.keys(_sidebar)
.sort((a, b) => {
return b.split('/').length - a.split('/').length;
})
.find((dir) => {
// make sure the multi sidebar key starts with slash too
return path.startsWith(ensureStartingSlash(dir));
});
const sidebar = dir ? _sidebar[dir] : [];
return Array.isArray(sidebar)
? addBase(sidebar)
: addBase(sidebar.items, sidebar.base);
}
/**
* Get or generate sidebar group from the given sidebar items.
*/
export function getSidebarGroups(sidebar) {
const groups = [];
let lastGroupIndex = 0;
for (const index in sidebar) {
const item = sidebar[index];
if (item.items) {
lastGroupIndex = groups.push(item);
continue;
}
if (!groups[lastGroupIndex]) {
groups.push({ items: [] });
}
groups[lastGroupIndex].items.push(item);
}
return groups;
}
export function getFlatSideBarLinks(sidebar) {
const links = [];
function recursivelyExtractLinks(items) {
for (const item of items) {
if (item.text && item.link) {
links.push({
text: item.text,
link: item.link,
docFooterText: item.docFooterText
});
}
if (item.items) {
recursivelyExtractLinks(item.items);
}
}
}
recursivelyExtractLinks(sidebar);
return links;
}
/**
* Check if the given sidebar item contains any active link.
*/
export function hasActiveLink(path, items) {
if (Array.isArray(items)) {
return items.some((item) => hasActiveLink(path, item));
}
return isActive(path, items.link)
? true
: items.items
? hasActiveLink(path, items.items)
: false;
}
function addBase(items, _base) {
return [...items].map((_item) => {
const item = { ..._item };
const base = item.base || _base;
if (base && item.link)
item.link = base + item.link.replace(/^\//, base.endsWith('/') ? '' : '/');
if (item.items)
item.items = addBase(item.items, base);
return item;
});
}

View File

@@ -0,0 +1,49 @@
import { useData } from '../composables/data';
/**
* @param themeObject Can be an object with `translations` and `locales` properties
*/
export function createSearchTranslate(defaultTranslations) {
const { localeIndex, theme } = useData();
function translate(key) {
const keyPath = key.split('.');
const themeObject = theme.value.search?.options;
const isObject = themeObject && typeof themeObject === 'object';
const locales = (isObject && themeObject.locales?.[localeIndex.value]?.translations) ||
null;
const translations = (isObject && themeObject.translations) || null;
let localeResult = locales;
let translationResult = translations;
let defaultResult = defaultTranslations;
const lastKey = keyPath.pop();
for (const k of keyPath) {
let fallbackResult = null;
const foundInFallback = defaultResult?.[k];
if (foundInFallback) {
fallbackResult = defaultResult = foundInFallback;
}
const foundInTranslation = translationResult?.[k];
if (foundInTranslation) {
fallbackResult = translationResult = foundInTranslation;
}
const foundInLocale = localeResult?.[k];
if (foundInLocale) {
fallbackResult = localeResult = foundInLocale;
}
// Put fallback into unresolved results
if (!foundInFallback) {
defaultResult = fallbackResult;
}
if (!foundInTranslation) {
translationResult = fallbackResult;
}
if (!foundInLocale) {
localeResult = fallbackResult;
}
}
return (localeResult?.[lastKey] ??
translationResult?.[lastKey] ??
defaultResult?.[lastKey] ??
'');
}
return translate;
}

View File

@@ -0,0 +1,33 @@
import { withBase } from 'vitepress';
import { isExternal, treatAsHtml } from '../../shared';
import { useData } from '../composables/data';
export function throttleAndDebounce(fn, delay) {
let timeoutId;
let called = false;
return () => {
if (timeoutId)
clearTimeout(timeoutId);
if (!called) {
fn();
(called = true) && setTimeout(() => (called = false), delay);
}
else
timeoutId = setTimeout(fn, delay);
};
}
export function ensureStartingSlash(path) {
return path.startsWith('/') ? path : `/${path}`;
}
export function normalizeLink(url) {
const { pathname, search, hash, protocol } = new URL(url, 'http://a.com');
if (isExternal(url) ||
url.startsWith('#') ||
!protocol.startsWith('http') ||
!treatAsHtml(pathname))
return url;
const { site } = useData();
const normalizedPath = pathname.endsWith('/') || pathname.endsWith('.html')
? url
: url.replace(/(?:(^\.+)\/)?.*$/, `$1${pathname.replace(/(\.md)?$/, site.value.cleanUrls ? '' : '.html')}${search}${hash}`);
return withBase(normalizedPath);
}