diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index a059280810c..c9bfff2c238 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -35,6 +35,10 @@ export default defineConfig({ trailingSlash: 'always', integrations: [ starlight({ + // TODO(HiDeoo) Remove the overrides and associated files. + components: { + Search: './src/components/SearchOverride.astro', + }, title: 'Starlight', logo: { light: '/src/assets/logo-light.svg', @@ -70,6 +74,11 @@ export default defineConfig({ customCss: ['./src/assets/landing.css'], locales, sidebar: [ + // TODO(HiDeoo) Remove this autogenerated group. + { + label: 'Demo', + autogenerate: { directory: 'demo' }, + }, { label: 'Start Here', translations: { diff --git a/docs/package.json b/docs/package.json index c400b48f4f1..b869f354159 100644 --- a/docs/package.json +++ b/docs/package.json @@ -6,6 +6,10 @@ "scripts": { "test": "playwright install --with-deps chromium && playwright test", "dev": "astro dev", + "comment": "// TODO(HiDeoo) Remove these pagefind related scripts", + "pagefind": "pnpm run --stream /^pagefind:/", + "pagefind:build": "astro build && astro preview", + "pagefind:dev": "astro dev", "start": "astro dev", "build": "astro build", "preview": "astro preview", diff --git a/docs/src/assets/landing.css b/docs/src/assets/landing.css index 88f6c63525b..b2d1a722c2b 100644 --- a/docs/src/assets/landing.css +++ b/docs/src/assets/landing.css @@ -15,7 +15,7 @@ 60rem 30rem; } -[data-has-hero] header { +[data-has-hero] .page > header { border-bottom: 1px solid transparent; background-color: transparent; -webkit-backdrop-filter: blur(16px); diff --git a/docs/src/components/SearchOverride.astro b/docs/src/components/SearchOverride.astro new file mode 100644 index 00000000000..ded33346cc8 --- /dev/null +++ b/docs/src/components/SearchOverride.astro @@ -0,0 +1,64 @@ +--- +import Default from '@astrojs/starlight/components/Search.astro'; + +// TODO(HiDeoo) Delete this override. + +const { id } = Astro.locals.starlightRoute; + +const triggerType = + id === 'demo/trigger-filters' ? 'filters' : id === 'demo/trigger-search' ? 'search' : undefined; +--- + +{ + triggerType ? ( + + + + + + ) : ( + + + + ) +} + + + + diff --git a/docs/src/content/docs/demo/trigger-filters.md b/docs/src/content/docs/demo/trigger-filters.md new file mode 100644 index 00000000000..af50a74188b --- /dev/null +++ b/docs/src/content/docs/demo/trigger-filters.md @@ -0,0 +1,11 @@ +--- +title: Trigger Filters API +--- + +:::danger +// TODO(HiDeoo) Delete this page. +::: + +This page exists to test a public API for triggering filters in the Pagefind search component. + +When doing a search from this page, e.g. for the word `test`, search results should be filtered by default to only include results that have the filter `author` set to `Alice`. diff --git a/docs/src/content/docs/demo/trigger-search.md b/docs/src/content/docs/demo/trigger-search.md new file mode 100644 index 00000000000..16ef235b80c --- /dev/null +++ b/docs/src/content/docs/demo/trigger-search.md @@ -0,0 +1,11 @@ +--- +title: Trigger Search API +--- + +:::danger +// TODO(HiDeoo) Delete this page. +::: + +This page exists to test a public API for searching with the Pagefind search component. + +When opening the search modal from this page, a search for the word `test` should be performed by default. diff --git a/docs/src/content/docs/guides/i18n.mdx b/docs/src/content/docs/guides/i18n.mdx index 6e70b47b1b6..05808156abf 100644 --- a/docs/src/content/docs/guides/i18n.mdx +++ b/docs/src/content/docs/guides/i18n.mdx @@ -232,24 +232,6 @@ You can provide translations for additional languages you support — or overrid } ``` - Starlight’s search modal is powered by the [Pagefind](https://pagefind.app/) library. - You can set translations for Pagefind’s UI in the same JSON file using `pagefind` keys: - - ```json - { - "pagefind.clear_search": "Clear", - "pagefind.load_more": "Load more results", - "pagefind.search_label": "Search this site", - "pagefind.filters_label": "Filters", - "pagefind.zero_results": "No results for [SEARCH_TERM]", - "pagefind.many_results": "[COUNT] results for [SEARCH_TERM]", - "pagefind.one_result": "[COUNT] result for [SEARCH_TERM]", - "pagefind.alt_search": "No results for [SEARCH_TERM]. Showing results for [DIFFERENT_TERM] instead", - "pagefind.search_suggestion": "No results for [SEARCH_TERM]. Try one of the following searches:", - "pagefind.searching": "Searching for [SEARCH_TERM]..." - } - ``` - ### Extend translation schema diff --git a/eslint.config.mjs b/eslint.config.mjs index 2e4df6cfbb2..e4951ffa534 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -43,11 +43,6 @@ export default tseslint.config( }, }, }, - // Disabled typed linting in JavaScript files. - { - files: ['**/*.js', '**/*.cjs', '**/*.mjs'], - extends: [tseslint.configs.disableTypeChecked], - }, // Disable all formatting rules. prettierConfig, @@ -82,6 +77,14 @@ export default tseslint.config( // fallbacks for some types that may not be accessible in some user environments, e.g. i18n // keys for plugins. '@typescript-eslint/no-redundant-type-constituents': 'off', + // Allow for using async event handlers without needing to use an async IIFE. + '@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }], }, + }, + + // Disabled typed linting in JavaScript files. + { + files: ['**/*.js', '**/*.cjs', '**/*.mjs'], + extends: [tseslint.configs.disableTypeChecked], } ); diff --git a/packages/starlight/components/Search.astro b/packages/starlight/components/Search.astro index d8c80d82699..afbe150a5e1 100644 --- a/packages/starlight/components/Search.astro +++ b/packages/starlight/components/Search.astro @@ -1,17 +1,18 @@ --- import Icon from '../user-components/Icon.astro'; import project from 'virtual:starlight/project-context'; +import Pagefind from './pagefind/Pagefind.astro'; +import type { StarlightPagefindOptions } from './pagefind/starlight-pagefind'; -const pagefindTranslations = { - placeholder: Astro.locals.t('search.label'), - ...Object.fromEntries( - Object.entries(Astro.locals.t.all()) - .filter(([key]) => key.startsWith('pagefind.')) - .map(([key, value]) => [key.replace('pagefind.', ''), value]) - ), -}; +const clientTranslations: StarlightPagefindOptions['translations'] = {}; +for (const [key, value] of Object.entries(Astro.locals.t.all())) { + if (isClientTranslation(key)) clientTranslations[key] = value as string; +} +function isClientTranslation(key: string): key is keyof StarlightPagefindOptions['translations'] { + return key.startsWith('search.pagefind.client.'); +} -const dataAttributes: DOMStringMap = { 'data-translations': JSON.stringify(pagefindTranslations) }; +const dataAttributes: DOMStringMap = { 'data-translations': JSON.stringify(clientTranslations) }; if (project.trailingSlash === 'never') dataAttributes['data-strip-trailing-slash'] = ''; --- @@ -37,17 +38,17 @@ if (project.trailingSlash === 'never') dataAttributes['data-strip-trailing-slash - { + {/* TODO(HiDeoo) Restore dev mode warning. */} + + @@ -75,6 +76,7 @@ if (project.trailingSlash === 'never') dataAttributes['data-strip-trailing-slash + + + + + diff --git a/packages/starlight/components/pagefind/starlight-pagefind-api.ts b/packages/starlight/components/pagefind/starlight-pagefind-api.ts new file mode 100644 index 00000000000..f2fa0522fd9 --- /dev/null +++ b/packages/starlight/components/pagefind/starlight-pagefind-api.ts @@ -0,0 +1,41 @@ +import type { StarlightPagefind } from './starlight-pagefind'; +import type { PagefindSearchFragment } from './starlight-pagefind-js'; + +export type { PagefindUserConfig as StarlightPagefindOptions } from '../../schemas/pagefind'; + +/** Starlight Pagefind API. */ +export interface StarlightPagefindApi { + /** + * Set Pagefind selected filters to use in the search results. + * Filter names and values supplied are case-sensitive. + */ + triggerFilters: (filters: PagefindSearchFragment['filters']) => void; + /** Perform a search with the given query. */ + triggerSearch: (query: string) => void; +} + +/** + * Returns the Starlight Pagefind API, waiting for the Starlight Pagefind component to be defined + * if necessary. + */ +export async function getPagefindApi(): Promise { + const instance = await getOrWaitForStarlightPagefind(); + + return { + triggerFilters: (...args) => instance.triggerFilters(...args), + triggerSearch: (...args) => instance.triggerSearch(...args), + }; +} + +/** Starlight Pagefind instance. */ +let starlightPagefind: StarlightPagefind | null = null; + +/** Return a fully initialized Starlight Pagefind instance, waiting for it to be ready if necessary. */ +async function getOrWaitForStarlightPagefind(): Promise { + if (starlightPagefind) return starlightPagefind; + + await customElements.whenDefined('starlight-pagefind'); + starlightPagefind = document.querySelector('starlight-pagefind')!; + + return starlightPagefind; +} diff --git a/packages/starlight/components/pagefind/starlight-pagefind-filter.ts b/packages/starlight/components/pagefind/starlight-pagefind-filter.ts new file mode 100644 index 00000000000..e4ec0e19bb8 --- /dev/null +++ b/packages/starlight/components/pagefind/starlight-pagefind-filter.ts @@ -0,0 +1,88 @@ +import type { PagefindFilters } from './starlight-pagefind'; +import type { PagefindFilterCounts } from './starlight-pagefind-js'; + +/** + * Component rendering a Pagefind filter with its known values. + * + * @see {@link https://github.com/Pagefind/pagefind/blob/d7d0b3a0f0eb12661cd2eb894ad02f10687a4ca0/pagefind_ui/default/svelte/filters.svelte} + */ +export class StarlightPagefindFilter extends HTMLElement { + /** Pagefind filter details. */ + #name?: string; + #values: PagefindFilterCounts[string] = {}; + #selectedValues: PagefindFilters['selected'][string] = []; + #defaultOpen?: boolean; + #onToggleOpen?: (opened: boolean) => void; + #showEmptyValues?: boolean; + + /** Various references to elements commonly used in this component. */ + #details = this.querySelector('details')!; + #summary = this.querySelector('summary')!; + + /** Callback called when the component is added to the document. */ + connectedCallback() { + this.#details.addEventListener('toggle', this.#onDetailsToggle); + } + + /** Set the Pagefind filter and render it. */ + set pagefindFilter(filter: { + name: string; + values: PagefindFilterCounts[string]; + selectedValues: PagefindFilters['selected'][string]; + defaultOpen?: boolean; + onToggleOpen: (opened: boolean) => void; + showEmptyValues: boolean; + }) { + this.#name = filter.name; + this.#values = filter.values; + this.#selectedValues = filter.selectedValues; + this.#defaultOpen = filter.defaultOpen ?? false; + this.#onToggleOpen = filter.onToggleOpen; + this.#showEmptyValues = filter.showEmptyValues; + + this.#render(); + } + + /** Callback called when the details element is toggled. */ + #onDetailsToggle = () => { + this.#onToggleOpen?.(this.#details.open); + }; + + /** Render a filter with all its values. */ + #render() { + if (!this.#name) throw new Error('Trying to render a filter without a name.'); + + this.#summary.textContent = + this.#name.charAt(0).toLocaleUpperCase(document.documentElement.lang || 'en') + + this.#name.slice(1); + + if (this.#defaultOpen) { + this.#details.setAttribute('open', ''); + } + + for (const [value, count] of Object.entries(this.#values)) { + if (!this.#showEmptyValues && count === 0) continue; + + const container = document.createElement('div'); + + const input = document.createElement('input'); + input.type = 'checkbox'; + input.id = `${this.#name}-${value}`; + input.name = this.#name; + input.value = value; + input.checked = this.#selectedValues.includes(value); + input.dataset.slPagefindFilterName = this.#name; + input.dataset.slPagefindFilterValue = value; + container.appendChild(input); + + const label = document.createElement('label'); + label.setAttribute('for', input.id); + label.textContent = `${value} (${count})`; + container.appendChild(label); + + this.#details.appendChild(container); + } + } +} + +customElements.define('starlight-pagefind-filter', StarlightPagefindFilter); diff --git a/packages/starlight/components/pagefind/starlight-pagefind-js.ts b/packages/starlight/components/pagefind/starlight-pagefind-js.ts new file mode 100644 index 00000000000..c9640bdf3cf --- /dev/null +++ b/packages/starlight/components/pagefind/starlight-pagefind-js.ts @@ -0,0 +1,240 @@ +/** Pagefind client which does not seem to have definitions available anywhere. */ +export interface Pagefind { + filters: () => Promise; + mergeIndex: (url: string, options: PagefindIndexOptions) => Promise; + options: (options: PagefindIndexOptions) => Promise; + preload(query: string, options?: PagefindOptions): Promise; + search: (query: string, options?: PagefindOptions) => Promise; +} + +/** Pagefind client options. */ +interface PagefindOptions { + filters: PagefindSearchFragment['filters']; +} + +/** + * Below are various Pagefind types that are not published. + * @see {@link https://github.com/Pagefind/pagefind/blob/production-docs/pagefind_web_js/types/index.d.ts} + */ + +/** Global index options that can be passed to pagefind.options() */ +export type PagefindIndexOptions = { + /** Overrides the URL path that Pagefind uses to load its search bundle */ + basePath?: string | undefined; + /** Appends the given baseURL to all search results. May be a path, or a full domain */ + baseUrl?: string | undefined; + /** The maximum length of excerpts that Pagefind should generate for search results. Default to 30 */ + excerptLength?: number; + /** + * Multiply all rankings for this index by the given weight. + * + * Only applies in multisite setups, where one site should rank higher or lower than others. + */ + indexWeight?: number | undefined; + /** + * Merge this filter object into all search queries in this index. + * + * Only applies in multisite setups. + */ + mergeFilter?: object | undefined; + /** + * If set, will ass the search term as a query parameter under this key, for use with Pagefind's highlighting script. + */ + highlightParam?: string; + language?: string | undefined; + /** + * Whether an instance of Pagefind is the primary index or not (for multisite). + * + * This is set for you automatically, so it is unlikely you should set this directly. + */ + primary?: boolean; + /** + * Provides the ability to fine tune Pagefind's ranking algorithm to better suit your dataset. + */ + ranking?: PagefindRankingWeights; +}; + +type PagefindRankingWeights = { + /** + * Controls page ranking based on similarity of terms to the search query (in length). + * Increasing this number means pages rank higher when they contain words very close to the query, + * e.g. if searching for `part` then `party` will boost a page higher than one containing `partition`. + * Minimum value is 0.0, where `party` and `partition` would be viewed equally. + */ + termSimilarity?: number; + /** + * Controls how much effect the average page length has on ranking. + * Maximum value is 1.0, where ranking will strongly favour pages that are shorter than the average page on the site. + * Minimum value is 0.0, where ranking will exclusively look at term frequency, regardless of how long a document is. + */ + pageLength?: number; + /** + * Controls how quickly a term saturates on the page and reduces impact on the ranking. + * Maximum value is 2.0, where pages will take a long time to saturate, and pages with very high term frequencies will take over. + * As this number trends to 0, it does not take many terms to saturate and allow other paramaters to influence the ranking. + * Minimum value is 0.0, where terms will saturate immediately and results will not distinguish between one term and many. + */ + termSaturation?: number; + /** + * Controls how much ranking uses term frequency versus raw term count. + * Maximum value is 1.0, where term frequency fully applies and is the main ranking factor. + * Minimum value is 0.0, where term frequency does not apply, and pages are ranked based on the raw sum of words and weights. + * Values between 0.0 and 1.0 will interpolate between the two ranking methods. + * Reducing this number is a good way to boost longer documents in your search results, as they no longer get penalized for having a low term frequency. + */ + termFrequency?: number; +}; + +/** Filter counts returned from pagefind.filters(), and alongside results from pagefind.search() */ +export type PagefindFilterCounts = Record>; + +/** The main results object returned from a call to pagefind.search() */ +type PagefindSearchResults = { + /** All pages that match the search query and filters provided */ + results: PagefindSearchResult[]; + /** How many results would there have been if you had omitted the filters */ + unfilteredResultCount: number; + /** Given the query and filters provided, how many remaining results are there under each filter? */ + filters: PagefindFilterCounts; + /** If the searched filters were removed, how many total results for each filter are there? */ + totalFilters: PagefindFilterCounts; + /** Information on how long it took Pagefind to execute this query */ + timings: { + preload: number; + search: number; + total: number; + }; +}; + +/** A single result from a search query, before actual data has been loaded */ +export type PagefindSearchResult = { + /** Pagefind's internal ID for this page, unique across the site */ + id: string; + /** Pagefind's internal score for your query matching this page, that is used when ranking these results */ + score: number; + /** The locations of all matching words in this page */ + words: number[]; + /** + * Calling data() loads the final data fragment needed to display this result. + * + * Only call this when you need to display the data, rather than all at once. + * (e.g. one page as a time, or in a scroll listener) + * */ + data: () => Promise; +}; + +/** The useful data Pagefind provides for a search result */ +export type PagefindSearchFragment = { + /** Pagefind's processed URL for this page. Will include the baseUrl if configured */ + url: string; + /** Pagefind's unprocessed URL for this page */ + raw_url?: string; + /** The full processed content text of this page */ + content: string; + /** Internal type — ignore for now */ + raw_content?: string; + /** The processed excerpt for this result, with matching terms wrapping in `` elements */ + excerpt: string; + /** + * What regions of the page matched this search query? + * + * Precalculates based on h1->6 tags with IDs, using the text between each. + */ + sub_results: PagefindSubResult[]; + /** How many total words are there on this page? */ + word_count: number; + /** The locations of all matching words in this page */ + locations: number[]; + /** + * The locations of all matching words in this page, + * paired with data about their weight and relevance to this query + */ + weighted_locations: PagefindWordLocation[]; + /** The filter keys and values this page was tagged with */ + filters: Record; + /** The metadata keys and values this page was tagged with */ + meta: Record; + /** + * The raw anchor data that Pagefind used to generate sub_results. + * + * Contains _all_ elements that had IDs on the page, so can be used to + * implement your own sub result calculations with different semantics. + */ + anchors: PagefindSearchAnchor[]; +}; + +/** Data for a matched section within a page */ +export type PagefindSubResult = { + /** + * Title of this sub result — derived from the heading content. + * + * If this is a result for the section of the page before any headings with IDs, + * this will be the same as the page's meta.title value. + */ + title: string; + /** + * Direct URL to this sub result, comprised of the page's URL plus the hash string of the heading. + * + * If this is a result for the section of the page before any headings with IDs, + * this will be the same as the page URL. + */ + url: string; + /** The locations of all matching words in this segment */ + locations: number[]; + /** + * The locations of all matching words in this segment, + * paired with data about their weight and relevance to this query + */ + weighted_locations: PagefindWordLocation[]; + /** The processed excerpt for this segment, with matching terms wrapping in `` elements */ + excerpt: string; + /** + * Raw data about the anchor element associated with this sub result. + * + * The omission of this field means this sub result is for text found on the page + * before the first heading that had an ID. + */ + anchor?: PagefindSearchAnchor; +}; + +/** Information about a matching word on a page */ +type PagefindWordLocation = { + /** The weight that this word was originally tagged as */ + weight: number; + /** + * An internal score that Pagefind calculated for this word. + * + * The absolute value is somewhat meaningless, but the value can be used + * in comparison to other values in this set of search results to perform custom ranking. + */ + balanced_score: number; + /** + * The index of this word in the result content. + * + * Splitting the content key by whitespacing and indexing by this number + * will yield the correct word. + */ + location: number; +}; + +/** Raw data about elements with IDs that Pagefind encountered when indexing the page */ +type PagefindSearchAnchor = { + /** What element type was this anchor? e.g. `h1`, `div` */ + element: string; + /** The raw id="..." attribute contents of the element */ + id: string; + /** + * The text content of this element. + * + * In order to prevent repeating most of the page data for every anchor, + * Pagefind will only take top level text nodes, or text nodes nested within + * inline elements such as and . + */ + text?: string; + /** + * The position of this anchor in the result content. + * Splitting the content key by whitespacing and indexing by this number + * will yield the first word indexed after this element's ID was found. + */ + location: number; +}; diff --git a/packages/starlight/components/pagefind/starlight-pagefind-meta.ts b/packages/starlight/components/pagefind/starlight-pagefind-meta.ts new file mode 100644 index 00000000000..d83ad31736e --- /dev/null +++ b/packages/starlight/components/pagefind/starlight-pagefind-meta.ts @@ -0,0 +1,28 @@ +import type { StarlightPagefindSearchResult } from './starlight-pagefind'; + +/** Component rendering metadata of a Pagefind search result. */ +export class StarlightPagefindMeta extends HTMLElement { + /** Search result metadata to render. */ + #meta: StarlightPagefindSearchResult['meta'] = []; + + /** Various references to elements commonly used in this component. */ + #listitem = this.querySelector('li')!; + + /** Set the Pagefind metadata and render them. */ + set pagefindMeta(pagefindMeta: StarlightPagefindSearchResult['meta']) { + this.#meta = pagefindMeta; + + this.#render(); + } + + /** Render the metadata of a search result. */ + #render() { + for (const [key, value] of this.#meta) { + const div = document.createElement('div'); + div.textContent = `${key}: ${value}`; + this.#listitem.appendChild(div); + } + } +} + +customElements.define('starlight-pagefind-meta', StarlightPagefindMeta); diff --git a/packages/starlight/components/pagefind/starlight-pagefind-result.ts b/packages/starlight/components/pagefind/starlight-pagefind-result.ts new file mode 100644 index 00000000000..af7cdbd7348 --- /dev/null +++ b/packages/starlight/components/pagefind/starlight-pagefind-result.ts @@ -0,0 +1,60 @@ +import type { StarlightPagefindSearchResult } from './starlight-pagefind'; + +/** + * Component rendering a single Pagefind search result with potential sub-results. + * + * @see {@link https://github.com/Pagefind/pagefind/blob/d7d0b3a0f0eb12661cd2eb894ad02f10687a4ca0/pagefind_ui/default/svelte/result_with_subs.svelte} + */ +export class StarlightPagefindResult extends HTMLElement { + /** Starlight Pagefind search result ready to be rendered. */ + #searchResult?: StarlightPagefindSearchResult; + + /** Various references to elements commonly used in this component. */ + #link = this.querySelector('a')!; + + /** Set the Starlight Pagefind search result and render it. */ + set pagefindResult(pagefindResult: StarlightPagefindSearchResult) { + this.#searchResult = pagefindResult; + + this.#render(); + } + + /** Get the Pagefind ID of a search result. */ + get resultId() { + if (!this.#searchResult) { + throw new Error('Trying to access the pagefind ID of a result that has not been set yet.'); + } + + return this.#searchResult?.id; + } + + /** Mark this result as selected, e.g. when navigating with the keyboard. */ + set selected(selected: boolean) { + if (!selected) { + this.#link.removeAttribute('aria-selected'); + return; + } + + this.#link.setAttribute('aria-selected', 'true'); + } + + /** Render the search result. */ + #render() { + if (!this.#searchResult) throw new Error('Trying to render a search result that is not set.'); + + if (this.#searchResult.isSubResult) this.dataset.slPagefindSubResult = 'true'; + else this.dataset.slPagefindRootResult = 'true'; + + this.#link.id = this.#searchResult.id; + this.#link.href = this.#searchResult.href; + + this.#link.querySelector('.sl-pagefind-result-title')!.textContent = this.#searchResult.title; + + if (this.#searchResult.excerpt) { + this.#link.querySelector('.sl-pagefind-result-excerpt')!.innerHTML = + this.#searchResult.excerpt; + } + } +} + +customElements.define('starlight-pagefind-result', StarlightPagefindResult); diff --git a/packages/starlight/components/pagefind/starlight-pagefind.ts b/packages/starlight/components/pagefind/starlight-pagefind.ts new file mode 100644 index 00000000000..0fdc53d9a44 --- /dev/null +++ b/packages/starlight/components/pagefind/starlight-pagefind.ts @@ -0,0 +1,801 @@ +import type { PagefindConfig } from '../../schemas/pagefind'; +import type { + Pagefind, + PagefindFilterCounts, + PagefindIndexOptions, + PagefindSearchFragment, + PagefindSearchResult, + PagefindSubResult, +} from './starlight-pagefind-js'; +import type { StarlightPagefindResult } from './starlight-pagefind-result'; +import type { StarlightPagefindMeta } from './starlight-pagefind-meta'; +import type { StarlightPagefindFilter } from './starlight-pagefind-filter'; +import type { StarlightPagefindApi } from './starlight-pagefind-api'; + +/** + * Various configuration options for the Starlight Pagefind component that are either not + * user-configurable in Starlight or hardcoded values extracted from the original Pagefind UI. + */ +const starlightPagefindConfig = { + /** Amount of milliseconds to wait before performing a search after the user stops typing. */ + debounceTimeoutMs: 300, + /** Length of excerpts to show in search results. */ + excerptLength: 12, + /** Number of root results to show per page. */ + resultsPerPage: 5, + /** + * A list of meta keys to skip when processing results. + * @see {@link https://github.com/Pagefind/pagefind/blob/d7d0b3a0f0eb12661cd2eb894ad02f10687a4ca0/pagefind_ui/default/svelte/result_with_subs.svelte#L6} + */ + skipMeta: new Set(['title', 'image', 'image_alt', 'url']), + /** Number of sub-results to show per root result. */ + subResultsPerResult: 3, +}; + +/** + * A Starlight-specific rewrite of the original Pagefind UI. + * + * @see {@link https://github.com/Pagefind/pagefind/tree/main/pagefind_ui/default} + * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/} + * @see {@link https://www.makethingsaccessible.com/guides/accessible-site-search-with-combobox-suggestions/} + */ +export class StarlightPagefind extends HTMLElement implements StarlightPagefindApi { + /** Options for the Starlight Pagefind component. */ + #options: StarlightPagefindOptions = { bundlePath: '/pagefind/' }; + /** Pagefind options. */ + #pagefindOptions?: PagefindOptions; + + /** Various references to elements commonly used in this component. */ + #dialog = this.closest('dialog')!; + #form = this.querySelector('form')!; + #queryInput = this.querySelector('input[type="search"]')!; + #status = this.querySelector('.sl-pagefind-status')!; + #filters = this.querySelector('.sl-pagefind-filters')!; + #filterTemplate = this.querySelector('#sl-pagefind-filter-tpl')!; + #scrollable = this.#dialog.querySelector('.sl-pagefind-content')!; + #listbox = this.querySelector('ul')!; + #metaTemplate = this.querySelector('#sl-pagefind-meta-tpl')!; + #resultTemplate = this.querySelector('#sl-pagefind-result-tpl')!; + #clearButton = this.querySelector('button.sl-pagefind-clear')!; + #showMoreButton = this.querySelector('button.sl-pagefind-show-more')!; + #selectedResult?: StarlightPagefindResult | null; + + /** Pagefind instance and search results. */ + #pagefind?: Pagefind; + #pagefindResults: PagefindSearchResult[] = []; + #pagefindQuery = ''; + #pagefindFilters: PagefindFilters = { initial: {}, available: {}, opened: {}, selected: {} }; + #starlightPagefindResults: StarlightPagefindSearchResult[] = []; + + /** A match media query to check if getting a specific result into view should be animated or not. */ + #prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); + + /** Number formatter used to format numbers in translations. */ + #numberFormatter = new Intl.NumberFormat(document.documentElement.lang || 'en'); + + /** + * An abort controller and timeout reference used to orchestrate searches. + * @see {@link StarlightPagefind.#search} for more details. + */ + #seachAbortController?: AbortController | undefined; + #searchTimeout?: ReturnType; + + /** + * Various state variables used to track the state of the component. + * These states are not mutually exclusive, so multiple states can be true at the same time. + */ + #didTryLoadingPagefind = false; + #isPerformingSearch = false; + + /** Callback called when the component is added to the document. */ + connectedCallback() { + this.#attachEventListeners(); + } + + /* ------------ Event listeners ----------- */ + + /** Attach all required event listeners. */ + #attachEventListeners() { + this.#form.addEventListener('change', this.#onFormChange); + + this.#queryInput.addEventListener('focus', this.#loadPagefind); + this.#queryInput.addEventListener('keydown', this.#onQueryInputKeyDown); + this.#queryInput.addEventListener('input', this.#onQueryInputChange); + + this.#showMoreButton.addEventListener('click', this.#onShowMoreButtonClick); + + this.#clearButton.addEventListener('click', this.#onClearButtonClick); + } + + /** Handle form changes, specifically checkbox inputs for filters. */ + #onFormChange = async (event: Event) => { + if (!(event.target instanceof HTMLInputElement) || event.target.type !== 'checkbox') return; + + const filters: PagefindFilters['selected'] = {}; + + // Collect all checked filters and immediately transform them into the expected format. + // This is a different approach than the original Pagefind UI which transforms them + // for each search. + for (const filter of this.#filters.querySelectorAll( + 'input[type="checkbox"]:checked' + )) { + const name = filter.dataset.slPagefindFilterName; + const value = filter.dataset.slPagefindFilterValue; + if (!name || !value) continue; + + filters[name] ??= []; + filters[name].push(value); + } + + this.#pagefindFilters.selected = filters; + await this.#search(); + }; + + /** Handle keydown events on the query input. */ + #onQueryInputKeyDown = (event: KeyboardEvent) => { + if (!(event.target instanceof HTMLInputElement)) return; + + // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/#keyboardinteraction + switch (event.key) { + case 'ArrowDown': { + event.preventDefault(); + this.#updateSelectedResult('next'); + break; + } + case 'ArrowUp': { + event.preventDefault(); + this.#updateSelectedResult('prev'); + break; + } + // The Home and End keys place the editing cursor at the beginning/end of the field. + // The wrapping behavior of the Up and Down arrows is used as a substitute for this. + // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-both/#kbd_label_listbox + case 'Enter': { + event.preventDefault(); + this.#openSelection(); + break; + } + } + }; + + /** Handle query input changes. */ + #onQueryInputChange = async (event: Event) => { + if (!(event.target instanceof HTMLInputElement)) return; + this.#pagefindQuery = event.target.value; + this.#renderClearButton(); + await this.#search(); + }; + + /** Handle clicks on the button to show more results. */ + #onShowMoreButtonClick = async (event: Event) => { + event.preventDefault(); + await this.#transformPagefindResults(); + this.#render(); + }; + + /** Handle clicks on the button to clear the query input. */ + #onClearButtonClick = () => { + this.#queryInput.value = ''; + this.#queryInput.focus(); + this.#clearPagefindResults(); + this.#renderClearButton(); + }; + + /* --------------- Pagefind --------------- */ + + /** Set the options for the Starlight Pagefind component and the Pagefind library. */ + init(starlightPagefindOptions: StarlightPagefindOptions, pagefindOptions: PagefindOptions) { + this.#options = { ...this.#options, ...starlightPagefindOptions }; + this.#pagefindOptions = pagefindOptions; + } + + /** Load Pagefind, set its options, merge indexes, and load filters. */ + #loadPagefind = async () => { + if (this.#didTryLoadingPagefind) return; + this.#didTryLoadingPagefind = true; + if (this.#pagefind) return; + if (!this.#pagefindOptions) throw new Error('Trying to load Pagefind with no options.'); + + // We only assign pagefind to `this.#pagefind` after potentially merging indexes. + let pagefind: Pagefind | undefined; + + const pagefindPath = `${this.#options.bundlePath}pagefind.js`; + + try { + pagefind = (await import(/* @vite-ignore */ pagefindPath)) as Pagefind; + } catch { + console.error(`Failed to load Pagefind from ${pagefindPath}`); + } + + await pagefind?.options({ + excerptLength: starlightPagefindConfig.excerptLength, + ...this.#pagefindOptions, + }); + + for (const index of this.#options.mergeIndex ?? []) { + const { bundlePath, ...indexOptions } = index; + await pagefind?.mergeIndex(bundlePath, indexOptions); + } + + if (pagefind) this.#pagefind = pagefind; + + await this.#loadFilters(); + }; + + /** Return a fully initialized Pagefind instance, waiting for it to be ready if necessary. */ + #getOrWaitForPagefind() { + if (this.#pagefind) return this.#pagefind; + + return new Promise((resolve) => { + const interval = setInterval(() => { + if (this.#pagefind) { + clearInterval(interval); + resolve(this.#pagefind); + } + }, 50); + }); + } + + /** Load Pagefind initial filters, if any. */ + async #loadFilters() { + if (!this.#pagefind) return; + + this.#pagefindFilters.initial = await this.#pagefind.filters(); + + if (Object.keys(this.#pagefindFilters.available).length === 0) { + this.#pagefindFilters.available = this.#pagefindFilters.initial; + } + } + + /** Start a search for a given query, debounced by a timeout. */ + async #search() { + if (this.#searchTimeout) clearTimeout(this.#searchTimeout); + + if (!this.#pagefindQuery) { + this.#clearPagefindResults(); + return; + } + + // Process the term before performing or preloading a search (the original Pagefind UI + // implementation only does this when performing a search which seems to be a bug). + const query = this.#options.processTerm + ? this.#options.processTerm(this.#pagefindQuery) + : this.#pagefindQuery; + + this.#searchTimeout = setTimeout(async () => { + this.#seachAbortController?.abort(); + this.#seachAbortController = new AbortController(); + const signal = this.#seachAbortController.signal; + + try { + await this.#performSearch(query, this.#pagefindFilters.selected, signal); + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + // Skip aborted searches. + return; + } + // Re-throw other errors. + throw error; + } + }, starlightPagefindConfig.debounceTimeoutMs); + + // Even if the search is debounced, preload results for the query. + const pagefind = await this.#getOrWaitForPagefind(); + await pagefind.preload(query, { filters: this.#pagefindFilters.selected }); + } + + /** + * Perform a search for a given query and select filters, if any. Each query is attached to a + * signal which can be used to abort a search if a new search is started. + * + * This approach is different from the original Pagefind UI which manually increments a counter + * to attach an ID to each search, and then only renders results for the latest search. + * The approach used here allows to abort a search in progress if a new search is started rather + * than having all of them completing and then only rendering the latest one. + */ + async #performSearch(query: string, filters: PagefindFilters['selected'], signal: AbortSignal) { + this.#isPerformingSearch = true; + this.#renderStatus(); + + const pagefind = await this.#getOrWaitForPagefind(); + signal.throwIfAborted(); + + const { results, filters: newFilters } = await pagefind.search(query, { filters }); + signal.throwIfAborted(); + + if (Object.keys(newFilters).length > 0) this.#pagefindFilters.available = newFilters; + + this.#setPagefindResults(results); + await this.#transformPagefindResults(); + signal.throwIfAborted(); + + this.#isPerformingSearch = false; + + this.#render(true); + } + + /** Reset results and render the component with no results. */ + #clearPagefindResults() { + this.#setPagefindResults([]); + this.#render(true); + } + + /** Set the results of a search, clearing the previous selection and results. */ + #setPagefindResults(results: PagefindSearchResult[]) { + this.#selectedResult = null; + this.#starlightPagefindResults = []; + this.#pagefindResults = results; + } + + /** + * Transform multiple `PagefindSearchResult` into `StarlightPagefindSearchResult` which are ready + * to be rendered by the Starlight Pagefind component and contain all the necessary data. + * Only results that have not been rendered yet and results that should be rendered on the next + * page are transformed. + * + * This approach is different from the original Pagefind UI which loads the data for each + * result in the component rendering a result when it is needed. Such approach can lead to + * results with data being intertwined with results still loading their data, which can be + * confusing, especially on slow machines or connections. + * The approach used here still follows the Pagefind recommendation of not loading all results at + * once, but instead loads a limited number of results at a time, and only when data for all of + * them is ready, they are rendered at once. + */ + async #transformPagefindResults() { + this.#starlightPagefindResults.push( + ...(await Promise.all( + this.#pagefindResults + // Only transform results that have not been rendered yet and that should be rendered on + // the next page. + .slice( + this.#starlightPagefindResults.length, + this.#starlightPagefindResults.length + starlightPagefindConfig.resultsPerPage + ) + .map(this.#transformPagefindResult) + )) + ); + } + + /** + * Transform a single `PagefindSearchResult` into a `StarlightPagefindSearchResult` ready to be + * rendered by the Starlight Pagefind component and containing all the necessary data. + * Sub-results are also transformed so they can be rendered as well. + * @see {@link StarlightPagefind.#transformPagefindResults} for more details. + */ + #transformPagefindResult = async ( + pagefindResult: PagefindSearchResult + ): Promise => { + let data = await pagefindResult.data(); + + if (this.#options.processResult) data = this.#options.processResult(data); + + const id = pagefindResult.id; + const hasRootSubResult = data.sub_results.at(0)?.url === (data.meta?.url || data.url); + + const result: StarlightPagefindSearchResult = { + id, + isSubResult: false, + href: data.meta.url || data.url, + meta: Object.entries(data.meta).filter(([key]) => !starlightPagefindConfig.skipMeta.has(key)), + subResults: this.#getPagefindSubResults( + id, + hasRootSubResult ? data.sub_results.slice(1) : data.sub_results + ), + title: data.meta.title || '', + }; + + if (hasRootSubResult) result.excerpt = data.excerpt; + + return result; + }; + + /** + * Returns the sorted sub-results for a given result ID, limited to + * {@link starlightPagefindConfig.subResultsPerResult}. + * + * @see {@link https://github.com/Pagefind/pagefind/blob/d7d0b3a0f0eb12661cd2eb894ad02f10687a4ca0/pagefind_ui/default/svelte/result_with_subs.svelte#L13-L24 | original implementation} + */ + #getPagefindSubResults(resultId: string, subResults: PagefindSubResult[]) { + return subResults + .toSorted((a, b) => b.locations.length - a.locations.length) + .slice(0, starlightPagefindConfig.subResultsPerResult) + .map((subResult, index) => { + return { + excerpt: subResult.excerpt, + href: subResult.url, + id: `${resultId}-${index}`, + isSubResult: true, + meta: [], + title: subResult.title, + }; + }); + } + + /* ------ Starlight Pagefind results ------ */ + + /** Update the selected result based on s specified type of selection change. */ + #updateSelectedResult(type: 'first' | 'prev' | 'next') { + switch (type) { + case 'prev': + case 'next': { + if (!this.#selectedResult) { + this.#selectedResult = + type === 'prev' ? this.#results.item(this.#results.length - 1) : this.#results.item(0); + break; + } + + const currentIndex = Array.from(this.#results).findIndex( + (result) => result.resultId === this.#selectedResult?.resultId + ); + + let newIndex = currentIndex + (type === 'prev' ? -1 : 1); + if (type === 'prev' && newIndex < 0) newIndex = this.#results.length - 1; + else if (type === 'next' && newIndex >= this.#results.length) newIndex = 0; + + this.#selectedResult = this.#results.item(newIndex); + break; + } + // Select the first result by default. + default: { + this.#selectedResult = this.#results.item(0); + break; + } + } + + this.#renderSelection(); + } + + /** A collection of all rendered root results, excluding sub-results. */ + get #rootResults() { + return this.#listbox.querySelectorAll( + 'starlight-pagefind-result[data-sl-pagefind-root-result]' + ); + } + + /** A collection of all rendered results, including sub-results. */ + get #results() { + return this.#listbox.querySelectorAll('starlight-pagefind-result'); + } + + /** + * Append a result to the listbox, creating a new `starlight-pagefind-result` element. + * This can be a root result or a sub-result. + */ + #appendResult(starlightPagefindResult: StarlightPagefindSearchResult) { + const [node, result] = this.#cloneComponentTemplate( + this.#resultTemplate, + 'starlight-pagefind-result' + ); + + this.#listbox.appendChild(node); + result.pagefindResult = starlightPagefindResult; + } + + /** + * Append metadatas to the listbox, creating a new `starlight-pagefind-meta` element. + */ + #appendMeta(starlightPagefindResultMeta: StarlightPagefindSearchResult['meta']) { + const [node, meta] = this.#cloneComponentTemplate( + this.#metaTemplate, + 'starlight-pagefind-meta' + ); + + this.#listbox.appendChild(node); + meta.pagefindMeta = starlightPagefindResultMeta; + } + + /* --------------- Rendering -------------- */ + + /** + * Really basic i18n function matching i18next's interpolation syntax with support for plurals. + * + * @see {@link https://www.i18next.com/translation-function/interpolation} + * @see {@link https://www.i18next.com/translation-function/plurals} + */ + #t( + key: keyof NonNullable, + interpolations?: Record + ): string { + let result = this.#options.translations?.[key] ?? key; + if (interpolations) { + for (const [name, value] of Object.entries(interpolations)) { + if (typeof value === 'number') { + // Only the `zero`, `one`, and `other` forms are supported which match the original + // Pagefind UI implementation. Based on feedback, we can add more forms later if needed. + const suffix = value === 1 ? 'one' : value === 0 ? 'zero' : 'other'; + const { [name]: omittedName, ...rest } = interpolations; + return this.#t(`${key}_${suffix}`, { + ...rest, + [name]: this.#numberFormatter.format(value), + }); + } else { + result = result.replace(new RegExp(`{{${name}}}`, 'g'), value); + } + } + } + return result; + } + + /** Clone a Starlight Pagefind component template and return the cloned node and the component itself. */ + #cloneComponentTemplate( + template: HTMLTemplateElement, + conponent: `starlight-pagefind-${string}` + ): [DocumentFragment, T] { + const node = template.content.cloneNode(true) as DocumentFragment; + const component = node.querySelector(conponent)!; + return [node, component]; + } + + /** Render the entire component, optionally clearing the listbox for new searches. */ + #render(isNewSearch = false) { + if (isNewSearch) this.#listbox.textContent = ''; + + this.#renderStatus(); + this.#renderFilters(); + this.#renderResults(); + this.#renderShowMoreButton(); + + // Add keyboard access to the scrollable area if it is scrollable. + if (this.#scrollable.scrollHeight > this.#scrollable.clientHeight) { + this.#scrollable.setAttribute('tabindex', '0'); + } else { + this.#scrollable.removeAttribute('tabindex'); + } + } + + /** Render a status message based on the current state of the search. */ + #renderStatus() { + if (!this.#queryInput.value) { + this.#status.textContent = ''; + return; + } else if (this.#isPerformingSearch) { + this.#status.textContent = this.#t('search.pagefind.client.searching', { + query: this.#queryInput.value, + }); + return; + } + + this.#status.textContent = this.#t('search.pagefind.client.results', { + count: this.#pagefindResults.length, + query: this.#queryInput.value, + }); + } + + /** Render the filters UI based on the available filters and the selected ones. */ + #renderFilters() { + const filters = Object.entries(this.#pagefindFilters.available); + + if (filters.length === 0 || !this.#queryInput.value) { + this.#filters.classList.add('sl-hidden'); + return; + } + + const focusedValue = Array.from(this.#filters.querySelectorAll('input')).findIndex( + (input) => input === document.activeElement + ); + + const legend = this.#filters.firstElementChild!; + this.#filters.textContent = ''; + this.#filters.appendChild(legend); + + for (const [name, values] of filters) { + const [node, filter] = this.#cloneComponentTemplate( + this.#filterTemplate, + 'starlight-pagefind-filter' + ); + + this.#filters.appendChild(node); + filter.pagefindFilter = { + name, + values, + selectedValues: this.#pagefindFilters.selected[name] ?? [], + // Open by default if there is only one filter, if the filter was previously opened, or if + // it is configured to be open by default. + defaultOpen: + filters.length === 1 || + (this.#pagefindFilters.opened[name] ?? false) || + (this.#options.openFilters ?? []).includes(name), + onToggleOpen: (opened) => { + this.#pagefindFilters.opened[name] = opened; + }, + showEmptyValues: this.#options.showEmptyFilters ?? true, + }; + } + + this.#filters.classList.remove('sl-hidden'); + + if (focusedValue !== -1) { + this.#filters.querySelectorAll('input')[focusedValue]?.focus(); + } + } + + /** Render results which are not already rendered. */ + #renderResults() { + // Skip root results that have already been rendered. + for (let i = this.#rootResults.length; i < this.#starlightPagefindResults.length; i++) { + const pagefindResult = this.#starlightPagefindResults[i]!; + this.#renderRootResult(pagefindResult); + } + + this.#queryInput.setAttribute( + 'aria-expanded', + String(this.#starlightPagefindResults.length > 0) + ); + } + + /** Render a root result, which may or may also render sub-results. */ + #renderRootResult(starlightPagefindResult: StarlightPagefindSearchResult) { + this.#appendResult(starlightPagefindResult); + + if (!starlightPagefindResult.subResults) return; + + for (const subResult of starlightPagefindResult.subResults) { + this.#appendResult(subResult); + } + + if (starlightPagefindResult.meta.length > 0) { + this.#appendMeta(starlightPagefindResult.meta); + } + } + + /** Render the "Show More" button if there are more results to show. */ + #renderShowMoreButton() { + if (this.#pagefindResults.length > this.#starlightPagefindResults.length) { + this.#showMoreButton.classList.remove('sl-hidden'); + } else { + this.#showMoreButton.classList.add('sl-hidden'); + } + } + + /** + * Render the selection based on the currently selected result, and optionally scroll it into + * view. + */ + #renderSelection() { + for (const result of this.#results) { + result.selected = result.resultId === this.#selectedResult?.resultId; + } + + if (!this.#selectedResult) { + this.#queryInput.removeAttribute('aria-activedescendant'); + return; + } + + this.#queryInput.setAttribute('aria-activedescendant', this.#selectedResult.resultId); + + this.#scrollSelectionIntoView(); + } + + /** Render the button to clear the query input if there is a value in it. */ + #renderClearButton() { + if (this.#queryInput.value) { + this.#clearButton.classList.remove('sl-hidden'); + this.#clearButton.classList.add('sl-flex'); + } else { + this.#clearButton.classList.add('sl-hidden'); + this.#clearButton.classList.remove('sl-flex'); + } + } + + /* --------------- Selection -------------- */ + + /** + * Open the currently selected result, if any. + * This is usually triggered when accepting a combobox result selection, e.g. by pressing the + * Enter key when the combobox input is focused. + * Clicking on the result itself will not trigger this method, as results are links. + */ + #openSelection() { + if (!this.#selectedResult) return; + this.#selectedResult.querySelector('a')?.click(); + } + + /** Scroll the currently selected result into view if it is not already visible. */ + #scrollSelectionIntoView() { + if (!this.#selectedResult) return; + + const selectionRect = this.#selectedResult.getBoundingClientRect(); + const scrollableRect = this.#scrollable.getBoundingClientRect(); + + const isSelectionVisible = + selectionRect.top >= scrollableRect.top && selectionRect.bottom <= scrollableRect.bottom; + + if (isSelectionVisible) return; + + let newScrollTop = this.#scrollable.scrollTop; + const scrollOffset = 16; // 1rem + + if (selectionRect.top < scrollableRect.top) { + // Element above the visible area → scrolling up + newScrollTop += selectionRect.top - scrollableRect.top - scrollOffset; + } else if (selectionRect.bottom > scrollableRect.bottom) { + // Element below the visible area → scrolling down + newScrollTop += selectionRect.bottom - scrollableRect.bottom + scrollOffset; + } + + this.#scrollable.scrollTo({ + top: newScrollTop, + behavior: this.#prefersReducedMotion.matches ? 'instant' : 'smooth', + }); + } + + /* -------------- Public API -------------- */ + + /** Set Pagefind selected filters. */ + triggerFilters(filters: PagefindSearchFragment['filters']) { + const selected: PagefindFilters['selected'] = {}; + + for (const [name, values] of Object.entries(filters)) { + for (const value of values) { + selected[name] ??= []; + selected[name].push(value); + } + } + + this.#pagefindFilters.selected = selected; + } + + /** Perform a search with the given query. */ + triggerSearch(query: string) { + this.#queryInput.value = query; + this.#queryInput.dispatchEvent(new InputEvent('input')); + } +} + +customElements.define('starlight-pagefind', StarlightPagefind); + +/** Starlight Pagefind options user-defined in the Starlight `pagefind` configuration. */ +type StarlightPagefindOptionsFromPagefindConfig = + | 'mergeIndex' + | 'openFilters' + | 'processTerm' + | 'showEmptyFilters'; + +/** Options specific to the Starlight Pagefind component. */ +export type StarlightPagefindOptions = Pick< + PagefindConfig, + StarlightPagefindOptionsFromPagefindConfig +> & { + /** + * The Pagefind bundle directory path. + * @see {@link https://pagefind.app/docs/ui/#bundle-path | the Pagefind documentation} for more + * details. + */ + bundlePath?: string; + /** + * A function to process results before they are rendered. + * @see {@link https://pagefind.app/docs/ui/#process-result | the Pagefind documentation} for more + * details. + */ + processResult?: (result: PagefindSearchFragment) => PagefindSearchFragment; + /** Translations for the Starlight Pagefind client UI. */ + translations?: Record<`search.pagefind.client.${string}`, string>; +}; + +/** Pagefind specific options that can be overridden in Starlight. */ +type PagefindOptions = Partial> & + Pick; + +/** + * A result ready to be rendered by the Starlight Pagefind component and containing all the + * necessary data. + * Sub-results are also included so they can be rendered as well. + * @see {@link StarlightPagefind.#transformPagefindResults} for more details. + */ +export interface StarlightPagefindSearchResult { + excerpt?: string; + href: string; + id: string; + isSubResult: boolean; + meta: [key: string, value: string][]; + subResults?: StarlightPagefindSearchResult[]; + title: string; +} + +/** Various Pagefind filters which can be used to filter search results. */ +export interface PagefindFilters { + initial: PagefindFilterCounts; + available: PagefindFilterCounts; + opened: Record; + selected: PagefindSearchFragment['filters']; +} diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts index 35be3200ea4..baa3e03c2ca 100644 --- a/packages/starlight/integrations/virtual-user-config.ts +++ b/packages/starlight/integrations/virtual-user-config.ts @@ -143,8 +143,11 @@ export function vitePluginStarlightUserConfig( ['', 'export const routeMiddleware = [\n'] as [string, string] ) .join('\n') + '];', + 'virtual:starlight/pagefind-config': + typeof opts.pagefind === 'object' && 'clientOptionsModule' in opts.pagefind + ? `export { default as pagefindUserConfig } from ${resolveId(opts.pagefind.clientOptionsModule)};` + : `export const pagefindUserConfig = ${JSON.stringify(opts.pagefind || {})}`, /** Map of modules exporting Starlight’s templating components. */ - 'virtual:starlight/pagefind-config': `export const pagefindUserConfig = ${JSON.stringify(opts.pagefind || {})}`, ...virtualComponentModules, } satisfies Record; diff --git a/packages/starlight/package.json b/packages/starlight/package.json index a15e788ca57..ed0609d73e4 100644 --- a/packages/starlight/package.json +++ b/packages/starlight/package.json @@ -37,6 +37,7 @@ "./loaders": "./loaders.ts", "./route-data": "./route-data.ts", "./types": "./types.ts", + "./pagefind": "./components/pagefind/starlight-pagefind-api.ts", "./expressive-code": { "types": "./expressive-code.d.ts", "default": "./expressive-code.mjs" @@ -63,7 +64,6 @@ "@astrojs/markdown-remark": "^7.0.0", "@astrojs/mdx": "^5.0.0", "@astrojs/sitemap": "^3.7.1", - "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", diff --git a/packages/starlight/schemas/i18n.ts b/packages/starlight/schemas/i18n.ts index 34fe7d6f5c8..6c46f74471f 100644 --- a/packages/starlight/schemas/i18n.ts +++ b/packages/starlight/schemas/i18n.ts @@ -21,7 +21,6 @@ interface i18nSchemaOpts { const defaultI18nSchema = () => z.object({ ...starlightI18nSchema().shape, - ...pagefindI18nSchema().shape, ...expressiveCodeI18nSchema().shape, }); /** Type of Starlight’s default i18n schema, including extensions from Pagefind and Expressive Code. */ @@ -61,7 +60,6 @@ export type i18nSchemaOutput = z.output>; export function builtinI18nSchema() { return z.object({ ...z.strictObject({ ...starlightI18nSchema().required().shape }).shape, - ...pagefindI18nSchema().shape, ...expressiveCodeI18nSchema().shape, }); } @@ -89,6 +87,42 @@ function starlightI18nSchema() { description: 'Warning displayed when opening the Search in a dev environment.', }), + 'search.pagefind.clear': z + .string() + .meta({ description: 'Text for the “Clear” input button in the search modal.' }), + + 'search.pagefind.filters': z + .string() + .meta({ description: 'Title for the filters section in the search modal.' }), + + 'search.pagefind.loadMore': z + .string() + .meta({ description: 'Text for the “Load more results” button in the search modal.' }), + + 'search.pagefind.client.searching': z + .string() + .meta({ description: 'Status text displayed in the search modal while searching.' }), + + 'search.pagefind.client.results_zero': z + .string() + .meta({ + description: 'Status text displayed in the search modal when there are no results.', + }), + + 'search.pagefind.client.results_one': z + .string() + .meta({ + description: + 'Status text displayed in the search modal when there is exactly one result.', + }), + + 'search.pagefind.client.results_other': z + .string() + .meta({ + description: + 'Status text displayed in the search modal when there are more than one result.', + }), + 'themeSelect.accessibleLabel': z .string() .meta({ description: 'Accessible label for the theme selection dropdown.' }), @@ -169,62 +203,6 @@ function starlightI18nSchema() { .partial(); } -function pagefindI18nSchema() { - return z - .object({ - 'pagefind.clear_search': z.string().meta({ - description: - 'Pagefind UI translation. English default value: `"Clear"`. See https://pagefind.app/docs/ui/#translations', - }), - - 'pagefind.load_more': z.string().meta({ - description: - 'Pagefind UI translation. English default value: `"Load more results"`. See https://pagefind.app/docs/ui/#translations', - }), - - 'pagefind.search_label': z.string().meta({ - description: - 'Pagefind UI translation. English default value: `"Search this site"`. See https://pagefind.app/docs/ui/#translations', - }), - - 'pagefind.filters_label': z.string().meta({ - description: - 'Pagefind UI translation. English default value: `"Filters"`. See https://pagefind.app/docs/ui/#translations', - }), - - 'pagefind.zero_results': z.string().meta({ - description: - 'Pagefind UI translation. English default value: `"No results for [SEARCH_TERM]"`. See https://pagefind.app/docs/ui/#translations', - }), - - 'pagefind.many_results': z.string().meta({ - description: - 'Pagefind UI translation. English default value: `"[COUNT] results for [SEARCH_TERM]"`. See https://pagefind.app/docs/ui/#translations', - }), - - 'pagefind.one_result': z.string().meta({ - description: - 'Pagefind UI translation. English default value: `"[COUNT] result for [SEARCH_TERM]"`. See https://pagefind.app/docs/ui/#translations', - }), - - 'pagefind.alt_search': z.string().meta({ - description: - 'Pagefind UI translation. English default value: `"No results for [SEARCH_TERM]. Showing results for [DIFFERENT_TERM] instead"`. See https://pagefind.app/docs/ui/#translations', - }), - - 'pagefind.search_suggestion': z.string().meta({ - description: - 'Pagefind UI translation. English default value: `"No results for [SEARCH_TERM]. Try one of the following searches:"`. See https://pagefind.app/docs/ui/#translations', - }), - - 'pagefind.searching': z.string().meta({ - description: - 'Pagefind UI translation. English default value: `"Searching for [SEARCH_TERM]..."`. See https://pagefind.app/docs/ui/#translations', - }), - }) - .partial(); -} - function expressiveCodeI18nSchema() { return z .object({ diff --git a/packages/starlight/schemas/pagefind.ts b/packages/starlight/schemas/pagefind.ts index 3a470b1ca98..6dc43015542 100644 --- a/packages/starlight/schemas/pagefind.ts +++ b/packages/starlight/schemas/pagefind.ts @@ -71,40 +71,100 @@ const pagefindIndexOptionsSchema = z.object({ ranking: pagefindRankingWeightsSchema.prefault({}), }); -const pagefindSchema = z.object({ - /** - * Configure how search results from the current website are weighted by Pagefind - * compared to results from other sites when using the `mergeIndex` option. - * - * @see https://pagefind.app/docs/multisite/#changing-the-weighting-of-individual-indexes - */ - indexWeight: indexWeightSchema, - /** Configure how search result rankings are calculated by Pagefind. */ - ranking: pagefindRankingWeightsSchema.prefault({}), - /** - * Configure how search indexes from different sites are merged by Pagefind. - * - * @see https://pagefind.app/docs/multisite/#searching-additional-sites-from-pagefind-ui - */ - mergeIndex: z - .array( - /** - * Each entry of this array represents a `PagefindIndexOptions` from pagefind. - * - * @see https://github.com/CloudCannon/pagefind/blob/v1.3.0/pagefind_web_js/lib/coupled_search.ts#L549 - */ - z.object({ - ...pagefindIndexOptionsSchema.shape, +const pagefindSchema = z + .object({ + /** + * Configure how search results from the current website are weighted by Pagefind + * compared to results from other sites when using the `mergeIndex` option. + * + * @see https://pagefind.app/docs/multisite/#changing-the-weighting-of-individual-indexes + */ + indexWeight: indexWeightSchema, + /** Configure how search result rankings are calculated by Pagefind. */ + ranking: pagefindRankingWeightsSchema.prefault({}), + /** + * Configure how search indexes from different sites are merged by Pagefind. + * + * @see https://pagefind.app/docs/multisite/#searching-additional-sites-from-pagefind-ui + */ + mergeIndex: z + .array( /** - * Set Pagefind’s `bundlePath` mergeIndex option. + * Each entry of this array represents a `PagefindIndexOptions` from pagefind. * - * @see https://pagefind.app/docs/multisite/#searching-additional-sites-from-pagefind-ui + * @see https://github.com/CloudCannon/pagefind/blob/v1.3.0/pagefind_web_js/lib/coupled_search.ts#L549 */ - bundlePath: z.string(), - }) - ) - .optional(), -}); + z.object({ + ...pagefindIndexOptionsSchema.shape, + /** + * Set Pagefind’s `bundlePath` mergeIndex option. + * + * @see https://pagefind.app/docs/multisite/#searching-additional-sites-from-pagefind-ui + */ + bundlePath: z.string(), + }) + ) + .optional(), + /** + * Define a list of filters that should be open by default when the search results are displayed. + * By default, a filter is open only if it's the only filter available. + * + * @default [] + */ + openFilters: z.string().array().optional(), + /** + * Defines a function called before performing a search that can be used to normalize the + * search term. + */ + processTerm: z.function({ input: [z.string()], output: z.string() }).optional(), + /** + * Configure if filter values with no results should be visible or not. + * + * @default true + */ + showEmptyFilters: z.boolean().optional(), + }) + .strict(); + +const pagefindModuleSchema = z + .object({ + /** + * The path to a JavaScript or TypeScript file containing a default export of options to + * pass to the Pagefind client. + * + * The value can be a path to a local JS/TS file relative to the root of your project, + * e.g. `'./src/pagefind.js'`, or an npm module specifier for a package you installed, + * e.g. `'@company/pagefind-config'`. + * + * Use `clientOptionsModule` when you need to configure options that are not serializable, + * such as `processTerm()`. + * + * When `clientOptionsModule` is set, all options must be set via the module file. Other + * inline options passed to the plugin in `astro.config.mjs` will be ignored. + * + * @example + * // astro.config.mjs + * // ... + * pagefind: { clientOptionsModule: './src/config/pagefind.ts' } + * // ... + * + * // src/config/pagefind.ts + * import type { StarlightPagefindOptions } from '@astrojs/starlight/pagefind'; + * + * export default { + * openFilters: ['author'], + * processTerm(term) { + * return term.replace(/aa/g, 'ā'); + * }, + * } satisfies StarlightPagefindOptions; + */ + clientOptionsModule: z.string(), + }) + .strict(); export const PagefindConfigSchema = () => pagefindSchema; +export const PagefindModuleConfigSchema = () => pagefindModuleSchema; export const PagefindConfigDefaults = () => pagefindSchema.parse({}); + +export type PagefindUserConfig = z.input; +export type PagefindConfig = z.output; diff --git a/packages/starlight/translations/ar.json b/packages/starlight/translations/ar.json index 5d02881748b..4dfc9658def 100644 --- a/packages/starlight/translations/ar.json +++ b/packages/starlight/translations/ar.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "إلغاء", "search.devWarning": "البحث متوفر فقط في بنيات اﻹنتاج. \n جرب بناء المشروع ومعاينته على جهازك", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "اختر سمة", "themeSelect.dark": "داكن", "themeSelect.light": "فاتح", diff --git a/packages/starlight/translations/ca.json b/packages/starlight/translations/ca.json index ebd3f2dae10..0e5fa0d7abd 100644 --- a/packages/starlight/translations/ca.json +++ b/packages/starlight/translations/ca.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Cancel·lar", "search.devWarning": "La cerca només està disponible a les versions de producció. \nProva de construir i previsualitzar el lloc per provar-ho localment.", + "search.pagefind.clear": "Netejar", + "search.pagefind.filters": "Filtres", + "search.pagefind.loadMore": "Carregar més resultats", + "search.pagefind.client.searching": "Cercant {{query}}…", + "search.pagefind.client.results_zero": "Cap resultat per a: {{query}}", + "search.pagefind.client.results_one": "{{count}} resultat per a: {{query}}", + "search.pagefind.client.results_other": "{{count}} resultats per a: {{query}}", "themeSelect.accessibleLabel": "Seleccionar tema", "themeSelect.dark": "Fosc", "themeSelect.light": "Clar", @@ -29,15 +36,5 @@ "expressiveCode.terminalWindowFallbackTitle": "Finestra del terminal", "fileTree.directory": "Directori", "builtWithStarlight.label": "Fet amb Starlight", - "pagefind.clear_search": "Netejar", - "pagefind.load_more": "Carregar més resultats", - "pagefind.search_label": "Cercar pàgina", - "pagefind.filters_label": "Filtres", - "pagefind.zero_results": "Cap resultat per a: [SEARCH_TERM]", - "pagefind.many_results": "[COUNT] resultats per a: [SEARCH_TERM]", - "pagefind.one_result": "[COUNT] resultat per a: [SEARCH_TERM]", - "pagefind.alt_search": "Cap resultat per a [SEARCH_TERM]. Mostrant resultats per a: [DIFFERENT_TERM]", - "pagefind.search_suggestion": "Cap resultat per a [SEARCH_TERM]. Prova alguna d’aquestes cerques:", - "pagefind.searching": "Cercant [SEARCH_TERM]...", "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/cs.json b/packages/starlight/translations/cs.json index 8b2e321051e..69337ecc2f9 100644 --- a/packages/starlight/translations/cs.json +++ b/packages/starlight/translations/cs.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Zrušit", "search.devWarning": "Vyhledávání je dostupné pouze v produkčních sestaveních. \nZkuste sestavit a zobrazit náhled webu a otestovat jej lokálně.", + "search.pagefind.clear": "Vyčistit", + "search.pagefind.filters": "Filtry", + "search.pagefind.loadMore": "Načíst další výsledky", + "search.pagefind.client.searching": "Hledám {{query}}…", + "search.pagefind.client.results_zero": "Žádný výsledek pro: {{query}}", + "search.pagefind.client.results_one": "{{count}} výsledek pro: {{query}}", + "search.pagefind.client.results_other": "Počet výsledků: {{count}} pro: {{query}}", "themeSelect.accessibleLabel": "Vyberte motiv", "themeSelect.dark": "Tmavý", "themeSelect.light": "Světlý", @@ -29,15 +36,5 @@ "expressiveCode.copyButtonCopied": "Zkopírováno!", "expressiveCode.copyButtonTooltip": "Kopíruj do schránky", "expressiveCode.terminalWindowFallbackTitle": "Terminál", - "pagefind.clear_search": "Vyčistit", - "pagefind.load_more": "Načíst další výsledky", - "pagefind.search_label": "Vyhledat stránku", - "pagefind.filters_label": "Filtry", - "pagefind.zero_results": "Žádný výsledek pro: [SEARCH_TERM]", - "pagefind.many_results": "počet výsledků: [COUNT] pro: [SEARCH_TERM]", - "pagefind.one_result": "[COUNT] výsledek pro: [SEARCH_TERM]", - "pagefind.alt_search": "Žádné výsledky pro [SEARCH_TERM]. Namísto toho zobrazuji výsledky pro: [DIFFERENT_TERM]", - "pagefind.search_suggestion": "Žádný výsledek pro [SEARCH_TERM]. Zkus nějaké z těchto hledání:", - "pagefind.searching": "Hledám [SEARCH_TERM]...", "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/da.json b/packages/starlight/translations/da.json index 307a5f4fdb5..6f373e5c01e 100644 --- a/packages/starlight/translations/da.json +++ b/packages/starlight/translations/da.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Annuller", "search.devWarning": "Søgning er kun tilgængeligt i produktions versioner. \nPrøv at bygge siden og forhåndsvis den for at teste det lokalt.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Vælg tema", "themeSelect.dark": "Mørk", "themeSelect.light": "Lys", diff --git a/packages/starlight/translations/de.json b/packages/starlight/translations/de.json index c9cf8856158..cd45929f30d 100644 --- a/packages/starlight/translations/de.json +++ b/packages/starlight/translations/de.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Strg", "search.cancelLabel": "Abbrechen", "search.devWarning": "Die Suche ist nur in Produktions-Builds verfügbar. \nVersuche, die Website zu bauen und in der Vorschau anzusehen, um sie lokal zu testen.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Farbschema wählen", "themeSelect.dark": "Dunkel", "themeSelect.light": "Hell", diff --git a/packages/starlight/translations/el.json b/packages/starlight/translations/el.json index 930e8647740..b36440515f4 100644 --- a/packages/starlight/translations/el.json +++ b/packages/starlight/translations/el.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Ακύρωση", "search.devWarning": "Η αναζήτηση είναι διαθέσιμη μόνο σε builds παραγωγής.\nΔοκίμασε να κάνεις build τον ιστότοπο και να τον προεπισκοπήσεις για να τον ελέγξεις τοπικά.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Επιλογή χρωματικού θέματος", "themeSelect.dark": "Σκοτεινό", "themeSelect.light": "Φωτεινό", diff --git a/packages/starlight/translations/en.json b/packages/starlight/translations/en.json index 5e968f8ce3c..890ad07afd2 100644 --- a/packages/starlight/translations/en.json +++ b/packages/starlight/translations/en.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Cancel", "search.devWarning": "Search is only available in production builds. \nTry building and previewing the site to test it out locally.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Select theme", "themeSelect.dark": "Dark", "themeSelect.light": "Light", diff --git a/packages/starlight/translations/es.json b/packages/starlight/translations/es.json index e2653ca5b9f..fbf45da3631 100644 --- a/packages/starlight/translations/es.json +++ b/packages/starlight/translations/es.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Cancelar", "search.devWarning": "La búsqueda solo está disponible en las versiones de producción. \nIntenta construir y previsualizar el sitio para probarlo localmente.", + "search.pagefind.clear": "Limpiar", + "search.pagefind.filters": "Filtros", + "search.pagefind.loadMore": "Cargar más resultados", + "search.pagefind.client.searching": "Buscando {{query}}…", + "search.pagefind.client.results_zero": "Ningún resultado para: {{query}}", + "search.pagefind.client.results_one": "{{count}} resultado para: {{query}}", + "search.pagefind.client.results_other": "{{count}} resultados para: {{query}}", "themeSelect.accessibleLabel": "Seleccionar tema", "themeSelect.dark": "Oscuro", "themeSelect.light": "Claro", @@ -29,15 +36,5 @@ "expressiveCode.terminalWindowFallbackTitle": "Ventana de terminal", "fileTree.directory": "Directorio", "builtWithStarlight.label": "Hecho con Starlight", - "pagefind.clear_search": "Limpiar", - "pagefind.load_more": "Cargar más resultados", - "pagefind.search_label": "Buscar página", - "pagefind.filters_label": "Filtros", - "pagefind.zero_results": "Ningún resultado para: [SEARCH_TERM]", - "pagefind.many_results": "[COUNT] resultados para: [SEARCH_TERM]", - "pagefind.one_result": "[COUNT] resultado para: [SEARCH_TERM]", - "pagefind.alt_search": "Ningún resultado para [SEARCH_TERM]. Mostrando resultados para: [DIFFERENT_TERM]", - "pagefind.search_suggestion": "Ningún resultado para [SEARCH_TERM]. Prueba alguna de estas búsquedas:", - "pagefind.searching": "Buscando [SEARCH_TERM]...", "heading.anchorLabel": "Sección titulada «{{title}}»" } diff --git a/packages/starlight/translations/fa.json b/packages/starlight/translations/fa.json index 22db252ddf4..4fce655e55a 100644 --- a/packages/starlight/translations/fa.json +++ b/packages/starlight/translations/fa.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "لغو", "search.devWarning": "جستجو تنها در نسخه‌های تولیدی در دسترس است. \nسعی کنید سایت را بسازید و پیش‌نمایش آن را به صورت محلی آزمایش کنید.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "انتخاب پوسته", "themeSelect.dark": "تیره", "themeSelect.light": "روشن", diff --git a/packages/starlight/translations/fi.json b/packages/starlight/translations/fi.json index 520ff408eb8..da0de4de61a 100644 --- a/packages/starlight/translations/fi.json +++ b/packages/starlight/translations/fi.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Peruuta", "search.devWarning": "Haku on käytettävissä vain tuotantoversioissa.\nKokeile kääntää ja esikatsella sivustoa paikallisesti testataksesi sitä.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Valitse teema", "themeSelect.dark": "Tumma", "themeSelect.light": "Vaalea", diff --git a/packages/starlight/translations/fr.json b/packages/starlight/translations/fr.json index 25e6cea8108..327f677ab4d 100644 --- a/packages/starlight/translations/fr.json +++ b/packages/starlight/translations/fr.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Annuler", "search.devWarning": "La recherche est disponible uniquement en mode production. \nEssayez de construire puis de prévisualiser votre site pour tester la recherche localement.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Selectionner le thème", "themeSelect.dark": "Sombre", "themeSelect.light": "Clair", diff --git a/packages/starlight/translations/gl.json b/packages/starlight/translations/gl.json index 71656223045..7b17770eed8 100644 --- a/packages/starlight/translations/gl.json +++ b/packages/starlight/translations/gl.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Deixar", "search.devWarning": "A busca só está dispoñible nas versións de producción. \nTrata de construir e ollear o sitio para probalo localmente.", + "search.pagefind.clear": "Limpar", + "search.pagefind.filters": "Filtros", + "search.pagefind.loadMore": "Cargar máis resultados", + "search.pagefind.client.searching": "Buscando {{query}}…", + "search.pagefind.client.results_zero": "Ningún resultado para: {{query}}", + "search.pagefind.client.results_one": "{{count}} resultado para: {{query}}", + "search.pagefind.client.results_other": "{{count}} resultados para: {{query}}", "themeSelect.accessibleLabel": "Selecciona tema", "themeSelect.dark": "Escuro", "themeSelect.light": "Claro", @@ -29,15 +36,5 @@ "expressiveCode.terminalWindowFallbackTitle": "Fiestra do terminal", "fileTree.directory": "Directorio", "builtWithStarlight.label": "Feito con Starlight", - "pagefind.clear_search": "Limpar", - "pagefind.load_more": "Cargar máis resultados", - "pagefind.search_label": "Buscar páxina", - "pagefind.filters_label": "Filtros", - "pagefind.zero_results": "Ningún resultado para: [SEARCH_TERM]", - "pagefind.many_results": "[COUNT] resultados para: [SEARCH_TERM]", - "pagefind.one_result": "[COUNT] resultado para: [SEARCH_TERM]", - "pagefind.alt_search": "Ningún resultado para [SEARCH_TERM]. Amósanse resultados para: [DIFFERENT_TERM]", - "pagefind.search_suggestion": "Ningún resultado para [SEARCH_TERM]. Proba algunha destas buscas:", - "pagefind.searching": "Buscando [SEARCH_TERM]...", "heading.anchorLabel": "Sección titulada «{{title}}»" } diff --git a/packages/starlight/translations/he.json b/packages/starlight/translations/he.json index 0728103410d..54fe92f2492 100644 --- a/packages/starlight/translations/he.json +++ b/packages/starlight/translations/he.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "ביטול", "search.devWarning": "החיפוש זמין רק בסביבת ייצור. \nנסו לבנות ולהציג תצוגה מקדימה של האתר כדי לבדוק אותו באופן מקומי.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "בחרו פרופיל צבע", "themeSelect.dark": "כהה", "themeSelect.light": "בהיר", diff --git a/packages/starlight/translations/hi.json b/packages/starlight/translations/hi.json index 6cd11a0ca76..ede8ab1a6cc 100644 --- a/packages/starlight/translations/hi.json +++ b/packages/starlight/translations/hi.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "रद्द करे", "search.devWarning": "खोज केवल उत्पादन बिल्ड में उपलब्ध है। \nस्थानीय स्तर पर परीक्षण करने के लिए साइट बनाए और उसका पूर्वावलोकन करने का प्रयास करें।", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "थीम चुनें", "themeSelect.dark": "अँधेरा", "themeSelect.light": "रोशनी", diff --git a/packages/starlight/translations/hu.json b/packages/starlight/translations/hu.json index d94fb7b1430..a96fadd3d85 100644 --- a/packages/starlight/translations/hu.json +++ b/packages/starlight/translations/hu.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Mégsem", "search.devWarning": "A keresés csak a production build-ekben működik. \nPróbáld meg először buildelni, hogy kipróbálhasd.", + "search.pagefind.clear": "Törlés", + "search.pagefind.filters": "Szűrők", + "search.pagefind.loadMore": "Több találat betöltése", + "search.pagefind.client.searching": "Keresés erre: {{query}}…", + "search.pagefind.client.results_zero": "Erre a kifejezésre nincs találat: {{query}}", + "search.pagefind.client.results_one": "{{count}} találat erre: {{query}}", + "search.pagefind.client.results_other": "{{count}} találat erre: {{query}}", "themeSelect.accessibleLabel": "Téma választás", "themeSelect.dark": "Sötét", "themeSelect.light": "Világos", @@ -29,15 +36,5 @@ "heading.anchorLabel": "Szekció neve “{{title}}”", "expressiveCode.copyButtonCopied": "Másolva!", "expressiveCode.copyButtonTooltip": "Másolás", - "expressiveCode.terminalWindowFallbackTitle": "Terminál", - "pagefind.clear_search": "Törlés", - "pagefind.load_more": "Több találat betöltése", - "pagefind.search_label": "Keresés ezen az oldalon", - "pagefind.filters_label": "Szűrők", - "pagefind.zero_results": "Erre a kifejezésre nincs találat: [SEARCH_TERM]", - "pagefind.many_results": "[COUNT] találat erre: [SEARCH_TERM]", - "pagefind.one_result": "[COUNT] találat erre: [SEARCH_TERM]", - "pagefind.alt_search": "Erre a kifejezésre nincs találat: [SEARCH_TERM]. Találatok mutatása erre: [DIFFERENT_TERM]", - "pagefind.search_suggestion": "Erre a kifejezésre nincs találat: [SEARCH_TERM]. Próbáld meg ezek közül az egyiket:", - "pagefind.searching": "Keresés erre: [SEARCH_TERM]..." + "expressiveCode.terminalWindowFallbackTitle": "Terminál" } diff --git a/packages/starlight/translations/id.json b/packages/starlight/translations/id.json index b42d3f3b7ad..36043e79937 100644 --- a/packages/starlight/translations/id.json +++ b/packages/starlight/translations/id.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Batal", "search.devWarning": "Pencarian hanya tersedia pada build produksi. \nLakukan proses build dan pratinjau situs Anda sebelum mencoba di lingkungan lokal.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Pilih tema", "themeSelect.dark": "Gelap", "themeSelect.light": "Terang", diff --git a/packages/starlight/translations/it.json b/packages/starlight/translations/it.json index 65c32f15b6f..c3c31b29e28 100644 --- a/packages/starlight/translations/it.json +++ b/packages/starlight/translations/it.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Cancella", "search.devWarning": "La ricerca è disponibile solo nelle build di produzione. \nProvare ad eseguire il processo di build e visualizzare la preview del sito per testarlo localmente.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Seleziona tema", "themeSelect.dark": "Scuro", "themeSelect.light": "Chiaro", diff --git a/packages/starlight/translations/ja.json b/packages/starlight/translations/ja.json index 2e1fd806968..209adda27ba 100644 --- a/packages/starlight/translations/ja.json +++ b/packages/starlight/translations/ja.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "キャンセル", "search.devWarning": "検索はプロダクションビルドでのみ利用可能です。\nローカルでテストするには、サイトをビルドしてプレビューしてください。", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "テーマの選択", "themeSelect.dark": "ダーク", "themeSelect.light": "ライト", diff --git a/packages/starlight/translations/ko.json b/packages/starlight/translations/ko.json index c8db5972fec..a066f7cfe76 100644 --- a/packages/starlight/translations/ko.json +++ b/packages/starlight/translations/ko.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "취소", "search.devWarning": "검색 기능은 프로덕션 빌드에서만 작동합니다. \n로컬에서 테스트하려면 사이트를 빌드하고 미리 보기를 실행하세요.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "테마 선택", "themeSelect.dark": "어두운 테마", "themeSelect.light": "밝은 테마", diff --git a/packages/starlight/translations/lv.json b/packages/starlight/translations/lv.json index 43190fc987b..23a2164443b 100644 --- a/packages/starlight/translations/lv.json +++ b/packages/starlight/translations/lv.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Atcelt", "search.devWarning": "Meklēšana ir pieejama tikai ražošanas kompilācijās. \nMēģiniet kompilēt un priekšskatīt vietni, lai to pārbaudītu lokāli.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Izvēlieties tēmu", "themeSelect.dark": "Tumša", "themeSelect.light": "Gaiša", diff --git a/packages/starlight/translations/nb.json b/packages/starlight/translations/nb.json index 892fc435e2d..31aac438f6e 100644 --- a/packages/starlight/translations/nb.json +++ b/packages/starlight/translations/nb.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Avbryt", "search.devWarning": "Søk er bare tilgjengelig i produksjonsbygg. \nPrøv å bygg siden og forhåndsvis den for å teste det lokalt.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Velg tema", "themeSelect.dark": "Mørk", "themeSelect.light": "Lys", diff --git a/packages/starlight/translations/nl.json b/packages/starlight/translations/nl.json index e2a6cbe7c4f..bf6f4ac40c3 100644 --- a/packages/starlight/translations/nl.json +++ b/packages/starlight/translations/nl.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Annuleren", "search.devWarning": "Zoeken is alleen beschikbaar tijdens productie. \nProbeer om de site te builden en er een preview van te bekijken om lokaal te testen.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Selecteer thema", "themeSelect.dark": "Donker", "themeSelect.light": "Licht", diff --git a/packages/starlight/translations/pl.json b/packages/starlight/translations/pl.json index 8a0c475930d..e2e223016a6 100644 --- a/packages/starlight/translations/pl.json +++ b/packages/starlight/translations/pl.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Anuluj", "search.devWarning": "Wyszukiwanie jest dostępne tylko w buildach produkcyjnych. \nSpróbuj zbudować i uruchomić aplikację, aby przetestować lokalnie.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Wybierz motyw", "themeSelect.dark": "Ciemny", "themeSelect.light": "Jasny", diff --git a/packages/starlight/translations/pt.json b/packages/starlight/translations/pt.json index 25b57b2fd5b..f28dd5619ce 100644 --- a/packages/starlight/translations/pt.json +++ b/packages/starlight/translations/pt.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Cancelar", "search.devWarning": "A pesquisa está disponível apenas em builds em produção. \nTente fazer a build e pré-visualize o site para testar localmente.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Selecionar tema", "themeSelect.dark": "Escuro", "themeSelect.light": "Claro", diff --git a/packages/starlight/translations/ro.json b/packages/starlight/translations/ro.json index e7083649c86..07986a424de 100644 --- a/packages/starlight/translations/ro.json +++ b/packages/starlight/translations/ro.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Anulează", "search.devWarning": "Căutarea este disponibilă numai în versiunea de producție. \nÎncercă să construiești și să previzualizezi site-ul pentru a-l testa local.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Selectează tema", "themeSelect.dark": "Întunecată", "themeSelect.light": "Deschisă", diff --git a/packages/starlight/translations/ru.json b/packages/starlight/translations/ru.json index 1d97ae79e44..fc956dd6a64 100644 --- a/packages/starlight/translations/ru.json +++ b/packages/starlight/translations/ru.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Отменить", "search.devWarning": "Поиск доступен только в продакшен-сборках. \nВыполните сборку и запустите превью, чтобы протестировать поиск локально.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Выберите тему", "themeSelect.dark": "Тёмная", "themeSelect.light": "Светлая", diff --git a/packages/starlight/translations/sk.json b/packages/starlight/translations/sk.json index a54c83fb9c3..cd3793f137c 100644 --- a/packages/starlight/translations/sk.json +++ b/packages/starlight/translations/sk.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Zrušiť", "search.devWarning": "Vyhľadávanie je dostupné len v produkčných zostaveniach. \nSkúste vytvoriť a zobraziť náhľad stránky lokálne.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Vyberte tému", "themeSelect.dark": "Tmavý", "themeSelect.light": "Svetlý", diff --git a/packages/starlight/translations/sv.json b/packages/starlight/translations/sv.json index a834ea96090..4feb20c9c1e 100644 --- a/packages/starlight/translations/sv.json +++ b/packages/starlight/translations/sv.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Avbryt", "search.devWarning": "Sökfunktionen är endast tillgänglig i produktionsbyggen. \nProva att bygga och förhandsvisa siten för att testa det lokalt.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Välj tema", "themeSelect.dark": "Mörkt", "themeSelect.light": "Ljust", diff --git a/packages/starlight/translations/th.json b/packages/starlight/translations/th.json index ce2a780658c..155cccba3c9 100644 --- a/packages/starlight/translations/th.json +++ b/packages/starlight/translations/th.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "ยกเลิก", "search.devWarning": "การค้นหาสามารถใช้งานได้ในเฉพาะเวอร์ชันใช้งานจริงเท่านั้น\nโปรดลองบิลด์และดูตัวอย่างเว็บไซต์เพื่อทดสอบฟังก์ชันบนอุปกรณ์ของคุณ", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "เลือกธีม", "themeSelect.dark": "มืด", "themeSelect.light": "สว่าง", diff --git a/packages/starlight/translations/tr.json b/packages/starlight/translations/tr.json index 9ea485734ec..66104a77b74 100644 --- a/packages/starlight/translations/tr.json +++ b/packages/starlight/translations/tr.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "İptal", "search.devWarning": "Arama yalnızca üretim derlemelerinde kullanılabilir. \nYerel bilgisayarınızda test etmek için siteyi derleyin ve önizleme yapın.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Tema seç", "themeSelect.dark": "Koyu", "themeSelect.light": "Açık", diff --git a/packages/starlight/translations/uk.json b/packages/starlight/translations/uk.json index 5bcbd924a8b..0d967cdc578 100644 --- a/packages/starlight/translations/uk.json +++ b/packages/starlight/translations/uk.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Скасувати", "search.devWarning": "Пошук доступний лише у виробничих збірках. \nСпробуйте зібрати та переглянути сайт, щоби протестувати його локально", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Обрати тему", "themeSelect.dark": "Темна", "themeSelect.light": "Світла", diff --git a/packages/starlight/translations/vi.json b/packages/starlight/translations/vi.json index 44e3f10efa7..3b385e3b334 100644 --- a/packages/starlight/translations/vi.json +++ b/packages/starlight/translations/vi.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "Hủy", "search.devWarning": "Chức năng tìm kiếm chỉ có sẵn trong các phiên bản thật.\nHãy thử xây dựng và xem trước trang web để kiểm tra.", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "Chọn giao diện", "themeSelect.dark": "Tối", "themeSelect.light": "Sáng", diff --git a/packages/starlight/translations/zh-CN.json b/packages/starlight/translations/zh-CN.json index 17c0f4e98f3..4eb55e923a0 100644 --- a/packages/starlight/translations/zh-CN.json +++ b/packages/starlight/translations/zh-CN.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "取消", "search.devWarning": "搜索仅适用于生产版本。\n尝试构建并预览网站以在本地测试。", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "选择主题", "themeSelect.dark": "深色", "themeSelect.light": "浅色", diff --git a/packages/starlight/translations/zh-TW.json b/packages/starlight/translations/zh-TW.json index 001e1d77fe1..0527622d7a7 100644 --- a/packages/starlight/translations/zh-TW.json +++ b/packages/starlight/translations/zh-TW.json @@ -4,6 +4,13 @@ "search.ctrlKey": "Ctrl", "search.cancelLabel": "取消", "search.devWarning": "正式版本才能使用搜尋功能。\n如要在本地測試,請先建置並預覽網站。", + "search.pagefind.clear": "Clear", + "search.pagefind.filters": "Filters", + "search.pagefind.loadMore": "Load more results", + "search.pagefind.client.searching": "Searching for {{query}}…", + "search.pagefind.client.results_zero": "No results for {{query}}", + "search.pagefind.client.results_one": "{{count}} result for {{query}}", + "search.pagefind.client.results_other": "{{count}} results for {{query}}", "themeSelect.accessibleLabel": "選擇佈景主題", "themeSelect.dark": "深色", "themeSelect.light": "淺色", diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts index c4c9b64b313..b8f2350a001 100644 --- a/packages/starlight/utils/user-config.ts +++ b/packages/starlight/utils/user-config.ts @@ -5,7 +5,11 @@ import { ExpressiveCodeSchema } from '../schemas/expressiveCode'; import { FaviconSchema } from '../schemas/favicon'; import { HeadConfigSchema } from '../schemas/head'; import { LogoConfigSchema } from '../schemas/logo'; -import { PagefindConfigDefaults, PagefindConfigSchema } from '../schemas/pagefind'; +import { + PagefindConfigDefaults, + PagefindConfigSchema, + PagefindModuleConfigSchema, +} from '../schemas/pagefind'; import { SidebarItemSchema } from '../schemas/sidebar'; import { TitleConfigSchema, TitleTransformConfigSchema } from '../schemas/site-title'; import { SocialLinksSchema } from '../schemas/social'; @@ -199,6 +203,7 @@ const UserConfigSchema = z.object({ // Transform `true` to our default config object. .transform((val) => val && PagefindConfigDefaults()) .or(PagefindConfigSchema()) + .or(PagefindModuleConfigSchema()) .optional(), /** Specify paths to components that should override Starlight’s default components */ diff --git a/packages/starlight/virtual-internal.d.ts b/packages/starlight/virtual-internal.d.ts index 3002b4519e4..53b33184f1d 100644 --- a/packages/starlight/virtual-internal.d.ts +++ b/packages/starlight/virtual-internal.d.ts @@ -24,7 +24,7 @@ declare module 'virtual:starlight/route-middleware' { declare module 'virtual:starlight/pagefind-config' { export const pagefindUserConfig: Partial< - Extract + Extract >; } diff --git a/packages/starlight/vitest.config.ts b/packages/starlight/vitest.config.ts index e9e29379335..3ee896ae8d5 100644 --- a/packages/starlight/vitest.config.ts +++ b/packages/starlight/vitest.config.ts @@ -31,6 +31,8 @@ export default defineConfig({ */ // Main integration entrypoint — don’t think we’re able to test this directly currently. // 'index.ts', + // Starlight Pagefind web components. + 'components/pagefind/**', ], thresholds: { lines: 87, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f675fac8b0..0b2c09b39e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,9 +188,6 @@ importers: '@astrojs/sitemap': specifier: ^3.7.1 version: 3.7.1 - '@pagefind/default-ui': - specifier: ^1.3.0 - version: 1.3.0 '@types/hast': specifier: ^3.0.4 version: 3.0.4 @@ -1188,9 +1185,6 @@ packages: cpu: [x64] os: [darwin] - '@pagefind/default-ui@1.3.0': - resolution: {integrity: sha512-CGKT9ccd3+oRK6STXGgfH+m0DbOKayX6QGlq38TfE1ZfUcPc5+ulTuzDbZUnMo+bubsEOIypm4Pl2iEyzZ1cNg==} - '@pagefind/linux-arm64@1.3.0': resolution: {integrity: sha512-8lsxNAiBRUk72JvetSBXs4WRpYrQrVJXjlRRnOL6UCdBN9Nlsz0t7hWstRk36+JqHpGWOKYiuHLzGYqYAqoOnQ==} cpu: [arm64] @@ -5029,8 +5023,6 @@ snapshots: '@pagefind/darwin-x64@1.3.0': optional: true - '@pagefind/default-ui@1.3.0': {} - '@pagefind/linux-arm64@1.3.0': optional: true