Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/runtime/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import type { H3Event } from 'h3'
import { collectionQueryBuilder } from './internal/query'
import { generateNavigationTree } from './internal/navigation'
import { generateItemSurround } from './internal/surround'
import type { GenerateSearchSectionsOptions, SearchCollectionOptions, SearchResult } from './internal/search'
import type { GenerateSearchSectionsOptions, SearchCollectionOptions, SearchResult, Section } from './internal/search'
import { generateSearchSections, buildFTSIndex, queryFTS, resetFTSIndex } from './internal/search'
import { fetchQuery } from './internal/api'
import type { Collections, PageCollections, CollectionQueryBuilder, SurroundOptions, SQLOperator, QueryGroupFunction, ContentNavigationItem, DatabaseAdapter } from '@nuxt/content'
import { ref, toValue, watch, tryUseNuxtApp } from '#imports'
import type { MaybeRefOrGetter } from 'vue'

export type { SearchCollectionOptions, SearchResult, GenerateSearchSectionsOptions } from './internal/search'
export type { SearchCollectionOptions, SearchResult, GenerateSearchSectionsOptions, Section } from './internal/search'

interface ChainablePromise<T extends keyof PageCollections, R> extends Promise<R> {
where(field: keyof PageCollections[T] | string, operator: SQLOperator, value?: unknown): ChainablePromise<T, R>
Expand All @@ -31,7 +31,18 @@ export function queryCollectionItemSurroundings<T extends keyof PageCollections>
return chainablePromise(collection, qb => generateItemSurround(qb, path, opts))
}

export function queryCollectionSearchSections<T extends keyof PageCollections>(collection: T, opts?: GenerateSearchSectionsOptions) {
export function queryCollectionSearchSections<T extends keyof PageCollections, const K extends keyof PageCollections[T]>(
collection: T,
opts: Omit<GenerateSearchSectionsOptions, 'extraFields'> & { extraFields: K[] },
): ChainablePromise<T, Array<Section & Pick<PageCollections[T], K>>>
export function queryCollectionSearchSections<T extends keyof PageCollections>(
collection: T,
opts?: GenerateSearchSectionsOptions,
): ChainablePromise<T, Section[]>
export function queryCollectionSearchSections<T extends keyof PageCollections>(
collection: T,
opts?: GenerateSearchSectionsOptions,
) {
return chainablePromise(collection, qb => generateSearchSections(qb, opts))
}

Expand Down
27 changes: 19 additions & 8 deletions src/runtime/internal/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import type { MinimarkTree } from 'minimark'
import { pick } from './utils'
import type { CollectionQueryBuilder, DatabaseAdapter, PageCollectionItemBase } from '~/src/types'

type Section = {
// Path to the section
export type Section = {
/** Path to the section, including anchor for sub-headings (e.g. `/guide#installation`) */
id: string
// Title of the section
title: string
// Parents sections titles
/** Titles of all ancestor headings, from the page title down to the parent of this section */
titles: string[]
// Level of the section
level: number
// Content of the section
content: string
}

Expand Down Expand Up @@ -101,7 +98,18 @@ export type GenerateSearchSectionsOptions = {
maxHeading?: `h${1 | 2 | 3 | 4 | 5 | 6}`
}

export async function generateSearchSections<T extends PageCollectionItemBase>(queryBuilder: CollectionQueryBuilder<T>, opts?: GenerateSearchSectionsOptions) {
export async function generateSearchSections<T extends PageCollectionItemBase, const K extends keyof T>(
queryBuilder: CollectionQueryBuilder<T>,
opts: Omit<GenerateSearchSectionsOptions, 'extraFields'> & { extraFields: K[] },
): Promise<Array<Section & Pick<T, K>>>
export async function generateSearchSections<T extends PageCollectionItemBase>(
queryBuilder: CollectionQueryBuilder<T>,
opts?: GenerateSearchSectionsOptions,
): Promise<Section[]>
export async function generateSearchSections<T extends PageCollectionItemBase>(
queryBuilder: CollectionQueryBuilder<T>,
opts?: GenerateSearchSectionsOptions,
): Promise<Section[]> {
const { ignoredTags = [], extraFields = [], minHeading = 'h1', maxHeading = 'h6' } = opts || {}
const minLevel = headingLevel(minHeading)
const maxLevel = headingLevel(maxHeading)
Expand All @@ -114,7 +122,10 @@ export async function generateSearchSections<T extends PageCollectionItemBase>(q
return documents.flatMap(doc => splitPageIntoSections(doc, { ignoredTags, extraFields: extraFields as string[], minLevel, maxLevel }))
}

function splitPageIntoSections(page: SectionablePage, { ignoredTags, extraFields, minLevel, maxLevel }: { ignoredTags: string[], extraFields: Array<string>, minLevel: number, maxLevel: number }) {
function splitPageIntoSections(
page: SectionablePage,
{ ignoredTags, extraFields, minLevel, maxLevel }: { ignoredTags: string[], extraFields: Array<string>, minLevel: number, maxLevel: number },
): Section[] {
const body = (!page.body || page.body?.type === 'root') ? page.body : toHast(page.body as unknown as MinimarkTree) as MDCRoot
const path = (page.path ?? '')
const extraFieldsData = pick(extraFields)(page as unknown as Record<string, unknown>)
Expand Down
20 changes: 18 additions & 2 deletions src/runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import type { H3Event } from 'h3'
import { collectionQueryBuilder } from './internal/query'
import { generateNavigationTree } from './internal/navigation'
import { generateItemSurround } from './internal/surround'
import { type GenerateSearchSectionsOptions, generateSearchSections } from './internal/search'
import { type GenerateSearchSectionsOptions, type Section, generateSearchSections } from './internal/search'
import { fetchQuery } from './internal/api'
import type { Collections, CollectionQueryBuilder, PageCollections, SurroundOptions, SQLOperator, QueryGroupFunction } from '@nuxt/content'

export type { GenerateSearchSectionsOptions, Section } from './internal/search'

interface ChainablePromise<T extends keyof PageCollections, R> extends Promise<R> {
where(field: keyof PageCollections[T] | string, operator: SQLOperator, value?: unknown): ChainablePromise<T, R>
andWhere(groupFactory: QueryGroupFunction<PageCollections[T]>): ChainablePromise<T, R>
Expand All @@ -25,7 +27,21 @@ export function queryCollectionItemSurroundings<T extends keyof PageCollections>
return chainablePromise(event, collection, qb => generateItemSurround(qb, path, opts))
}

export function queryCollectionSearchSections<T extends keyof PageCollections>(event: H3Event, collection: T, opts?: GenerateSearchSectionsOptions) {
export function queryCollectionSearchSections<T extends keyof PageCollections, const K extends keyof PageCollections[T]>(
event: H3Event,
collection: T,
opts: Omit<GenerateSearchSectionsOptions, 'extraFields'> & { extraFields: K[] },
): ChainablePromise<T, Array<Section & Pick<PageCollections[T], K>>>
export function queryCollectionSearchSections<T extends keyof PageCollections>(
event: H3Event,
collection: T,
opts?: GenerateSearchSectionsOptions,
): ChainablePromise<T, Section[]>
export function queryCollectionSearchSections<T extends keyof PageCollections>(
event: H3Event,
collection: T,
opts?: GenerateSearchSectionsOptions,
) {
return chainablePromise(event, collection, qb => generateSearchSections(qb, opts))
}

Expand Down
21 changes: 18 additions & 3 deletions test/unit/generateSearchSections.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest'
import { describe, it, expect, expectTypeOf } from 'vitest'
import { generateSearchSections } from '../../src/runtime/internal/search'
import type { Section } from '../../src/runtime/internal/search'
import type { CollectionQueryBuilder, PageCollectionItemBase } from '../../src/types'

describe('generateSearchSections', () => {
Expand Down Expand Up @@ -272,14 +273,28 @@ describe('generateSearchSections', () => {
},
])
})

it('should narrow return type to Section & Pick<T, K> when extraFields is provided', async () => {
type DocItem = PageCollectionItemBase & { author: string }
const mockQB = createMockQueryBuilder<DocItem>([{
path: '/test',
title: 'Test Page',
description: '',
author: 'Jane Doe',
body: null,
}])

const result = await generateSearchSections(mockQB, { extraFields: ['author'] as const })
expectTypeOf(result).toEqualTypeOf<Array<Section & Pick<DocItem, 'author'>>>()
})
})

function createMockQueryBuilder(result: unknown[]) {
function createMockQueryBuilder<T extends PageCollectionItemBase>(result: unknown[]) {
const mockQueryBuilder = {
where: () => mockQueryBuilder,
select: () => mockQueryBuilder,
all: async () => result,
} as unknown as CollectionQueryBuilder<PageCollectionItemBase>
} as unknown as CollectionQueryBuilder<T>

return mockQueryBuilder
}
Loading