vocs 2.1.12 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/internal/config.d.ts +246 -9
- package/dist/internal/config.d.ts.map +1 -1
- package/dist/internal/config.js +38 -9
- package/dist/internal/config.js.map +1 -1
- package/dist/internal/llms.d.ts.map +1 -1
- package/dist/internal/llms.js +5 -1
- package/dist/internal/llms.js.map +1 -1
- package/dist/internal/markdown-imports.d.ts +2 -0
- package/dist/internal/markdown-imports.d.ts.map +1 -0
- package/dist/internal/markdown-imports.js +103 -0
- package/dist/internal/markdown-imports.js.map +1 -0
- package/dist/internal/search.client.d.ts +6 -0
- package/dist/internal/search.client.d.ts.map +1 -1
- package/dist/internal/search.client.js +23 -0
- package/dist/internal/search.client.js.map +1 -1
- package/dist/internal/search.d.ts +2 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +10 -14
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/vite-plugins.js +1 -1
- package/dist/internal/vite-plugins.js.map +1 -1
- package/dist/react/Link.d.ts.map +1 -1
- package/dist/react/Link.js +11 -6
- package/dist/react/Link.js.map +1 -1
- package/dist/react/internal/Search.d.ts.map +1 -1
- package/dist/react/internal/Search.js +5 -7
- package/dist/react/internal/Search.js.map +1 -1
- package/dist/server/openapi/assets.generated.js +4 -4
- package/dist/server/openapi/assets.generated.js.map +1 -1
- package/dist/styles/twoslash.css +1 -1
- package/package.json +1 -1
- package/src/internal/config.ts +294 -21
- package/src/internal/llms.test.ts +35 -0
- package/src/internal/llms.ts +6 -1
- package/src/internal/markdown-imports.ts +154 -0
- package/src/internal/search.client.ts +29 -0
- package/src/internal/search.test.ts +195 -0
- package/src/internal/search.ts +14 -14
- package/src/internal/vite-plugins.ts +1 -1
- package/src/react/Link.tsx +12 -7
- package/src/react/internal/Search.tsx +7 -10
- package/src/server/openapi/assets.generated.ts +4 -4
- package/src/styles/twoslash.css +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import type * as Estree from 'estree'
|
|
4
|
+
import type * as MdAst from 'mdast'
|
|
5
|
+
import remarkMdx from 'remark-mdx'
|
|
6
|
+
import remarkParse from 'remark-parse'
|
|
7
|
+
import { unified } from 'unified'
|
|
8
|
+
|
|
9
|
+
type MdxEsm = MdAst.RootContent & {
|
|
10
|
+
type: 'mdxjsEsm'
|
|
11
|
+
data?: {
|
|
12
|
+
estree?: Estree.Program
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type MdxJsxElement = MdAst.RootContent & {
|
|
17
|
+
type: 'mdxJsxFlowElement' | 'mdxJsxTextElement'
|
|
18
|
+
attributes?: unknown[]
|
|
19
|
+
children?: MdAst.RootContent[]
|
|
20
|
+
name?: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type MarkdownImport = {
|
|
24
|
+
source: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Replacement = {
|
|
28
|
+
end: number
|
|
29
|
+
start: number
|
|
30
|
+
value: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function inlineMarkdownImports(source: string, filePath: string): string {
|
|
34
|
+
return inline(source, path.resolve(filePath), new Set())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function inline(source: string, filePath: string, seen: Set<string>): string {
|
|
38
|
+
if (seen.has(filePath)) return ''
|
|
39
|
+
seen.add(filePath)
|
|
40
|
+
|
|
41
|
+
let tree: MdAst.Root
|
|
42
|
+
try {
|
|
43
|
+
tree = unified().use(remarkParse).use(remarkMdx).parse(source)
|
|
44
|
+
} catch {
|
|
45
|
+
return source
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const imports = new Map<string, MarkdownImport>()
|
|
49
|
+
const replacements: Replacement[] = []
|
|
50
|
+
|
|
51
|
+
collectImports(tree.children, imports, replacements)
|
|
52
|
+
collectComponentReplacements(tree, imports, filePath, seen, replacements)
|
|
53
|
+
|
|
54
|
+
if (replacements.length === 0) return source
|
|
55
|
+
return applyReplacements(source, replacements)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function collectImports(
|
|
59
|
+
children: MdAst.RootContent[],
|
|
60
|
+
imports: Map<string, MarkdownImport>,
|
|
61
|
+
replacements: Replacement[],
|
|
62
|
+
) {
|
|
63
|
+
for (const node of children) {
|
|
64
|
+
if (node.type !== 'mdxjsEsm') continue
|
|
65
|
+
|
|
66
|
+
const mdxEsm = node as MdxEsm
|
|
67
|
+
const declarations = mdxEsm.data?.estree?.body.filter(
|
|
68
|
+
(statement): statement is Estree.ImportDeclaration => statement.type === 'ImportDeclaration',
|
|
69
|
+
)
|
|
70
|
+
if (!declarations || declarations.length === 0) continue
|
|
71
|
+
|
|
72
|
+
let markdownDeclarations = 0
|
|
73
|
+
for (const declaration of declarations) {
|
|
74
|
+
const source = declaration.source.value
|
|
75
|
+
if (typeof source !== 'string' || !isMarkdownImport(source)) continue
|
|
76
|
+
markdownDeclarations++
|
|
77
|
+
|
|
78
|
+
const specifier = declaration.specifiers.find(
|
|
79
|
+
(specifier): specifier is Estree.ImportDefaultSpecifier =>
|
|
80
|
+
specifier.type === 'ImportDefaultSpecifier',
|
|
81
|
+
)
|
|
82
|
+
if (!specifier) continue
|
|
83
|
+
imports.set(specifier.local.name, { source })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (markdownDeclarations === declarations.length) {
|
|
87
|
+
const replacement = toReplacement(node, '')
|
|
88
|
+
if (replacement) replacements.push(replacement)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function collectComponentReplacements(
|
|
94
|
+
node: MdAst.Root | MdAst.RootContent,
|
|
95
|
+
imports: Map<string, MarkdownImport>,
|
|
96
|
+
filePath: string,
|
|
97
|
+
seen: Set<string>,
|
|
98
|
+
replacements: Replacement[],
|
|
99
|
+
) {
|
|
100
|
+
if (isMdxJsxElement(node)) {
|
|
101
|
+
const name = node.name ?? undefined
|
|
102
|
+
const markdownImport = name ? imports.get(name) : undefined
|
|
103
|
+
if (markdownImport && isStaticMdxComponent(node)) {
|
|
104
|
+
const importedPath = resolveMarkdownImport(markdownImport.source, filePath)
|
|
105
|
+
if (importedPath) {
|
|
106
|
+
const importedSource = fs.readFileSync(importedPath, 'utf-8')
|
|
107
|
+
const value = inline(importedSource, importedPath, new Set(seen))
|
|
108
|
+
const replacement = toReplacement(node, value)
|
|
109
|
+
if (replacement) replacements.push(replacement)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if ('children' in node && Array.isArray(node.children))
|
|
115
|
+
for (const child of node.children)
|
|
116
|
+
collectComponentReplacements(child, imports, filePath, seen, replacements)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isMdxJsxElement(node: MdAst.Root | MdAst.RootContent): node is MdxJsxElement {
|
|
120
|
+
return node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isStaticMdxComponent(node: MdxJsxElement): boolean {
|
|
124
|
+
return (node.attributes?.length ?? 0) === 0 && (node.children?.length ?? 0) === 0
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveMarkdownImport(source: string, filePath: string): string | undefined {
|
|
128
|
+
if (!source.startsWith('.') && !path.isAbsolute(source)) return undefined
|
|
129
|
+
|
|
130
|
+
const resolved = path.isAbsolute(source) ? source : path.resolve(path.dirname(filePath), source)
|
|
131
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) return resolved
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isMarkdownImport(source: string): boolean {
|
|
136
|
+
return /\.(md|mdx)$/.test(source)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function toReplacement(
|
|
140
|
+
node: MdAst.Root | MdAst.RootContent,
|
|
141
|
+
value: string,
|
|
142
|
+
): Replacement | undefined {
|
|
143
|
+
const start = node.position?.start.offset
|
|
144
|
+
const end = node.position?.end.offset
|
|
145
|
+
if (start === undefined || end === undefined) return undefined
|
|
146
|
+
return { end, start, value }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function applyReplacements(source: string, replacements: Replacement[]): string {
|
|
150
|
+
let result = source
|
|
151
|
+
for (const replacement of replacements.sort((a, b) => b.start - a.start))
|
|
152
|
+
result = result.slice(0, replacement.start) + replacement.value + result.slice(replacement.end)
|
|
153
|
+
return result
|
|
154
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Options as MiniSearchOptions,
|
|
3
|
+
SearchOptions as MiniSearchSearchOptions,
|
|
4
|
+
} from 'minisearch'
|
|
5
|
+
import type * as Config from './config.js'
|
|
6
|
+
|
|
1
7
|
export const searchFields = ['category', 'subtitle', 'text', 'title', 'titles'] as const
|
|
2
8
|
export const storeFields = [
|
|
3
9
|
'category',
|
|
@@ -40,3 +46,26 @@ export function tokenize(text: string): string[] {
|
|
|
40
46
|
|
|
41
47
|
return tokens.filter((w) => w.length > 0)
|
|
42
48
|
}
|
|
49
|
+
|
|
50
|
+
export namespace SearchConfig {
|
|
51
|
+
export function getIndexOptions(config?: Config.Config): MiniSearchOptions {
|
|
52
|
+
const index = config?.search.index ?? {}
|
|
53
|
+
return {
|
|
54
|
+
...index,
|
|
55
|
+
fields: index.fields ? [...index.fields] : [...searchFields],
|
|
56
|
+
storeFields: mergeStoreFields(index.storeFields),
|
|
57
|
+
tokenize: index.tokenize ?? tokenize,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getQueryOptions(config: Config.Config): MiniSearchSearchOptions {
|
|
62
|
+
return {
|
|
63
|
+
...config.search.query,
|
|
64
|
+
tokenize: config.search.query?.tokenize ?? config.search.index?.tokenize ?? tokenize,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function mergeStoreFields(fields: readonly string[] | undefined): string[] {
|
|
69
|
+
return [...new Set([...storeFields, ...(fields ?? [])])]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as os from 'node:os'
|
|
3
|
+
import * as path from 'node:path'
|
|
1
4
|
import type * as MdAst from 'mdast'
|
|
2
5
|
import { describe, expect, it } from 'vitest'
|
|
3
6
|
import * as Config from './config.js'
|
|
@@ -859,6 +862,40 @@ Some content here.
|
|
|
859
862
|
})
|
|
860
863
|
})
|
|
861
864
|
|
|
865
|
+
describe('SearchDocuments.fromConfig', () => {
|
|
866
|
+
it('indexes imported markdown content from file-backed pages', async () => {
|
|
867
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vocs-search-'))
|
|
868
|
+
const pagesDir = path.join(rootDir, 'src/pages')
|
|
869
|
+
fs.mkdirSync(pagesDir, { recursive: true })
|
|
870
|
+
|
|
871
|
+
fs.writeFileSync(
|
|
872
|
+
path.join(rootDir, 'notes.md'),
|
|
873
|
+
'# Notes\n\nSome indexable prose about widgets and links.',
|
|
874
|
+
)
|
|
875
|
+
fs.writeFileSync(
|
|
876
|
+
path.join(pagesDir, 'notes.mdx'),
|
|
877
|
+
`---
|
|
878
|
+
title: Notes
|
|
879
|
+
---
|
|
880
|
+
|
|
881
|
+
import Notes from '../../notes.md'
|
|
882
|
+
|
|
883
|
+
<Notes />`,
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
const documents = await Search.SearchDocuments.fromConfig(Config.define({ rootDir }))
|
|
887
|
+
|
|
888
|
+
expect(documents).toContainEqual(
|
|
889
|
+
expect.objectContaining({
|
|
890
|
+
href: '/notes#notes',
|
|
891
|
+
text: ' Some indexable prose about widgets and links.',
|
|
892
|
+
title: 'Notes',
|
|
893
|
+
type: 'page',
|
|
894
|
+
}),
|
|
895
|
+
)
|
|
896
|
+
})
|
|
897
|
+
})
|
|
898
|
+
|
|
862
899
|
describe('Search fields and storeFields', () => {
|
|
863
900
|
it('exports search fields', () => {
|
|
864
901
|
expect(Search.searchFields).toMatchInlineSnapshot(`
|
|
@@ -902,6 +939,65 @@ describe('Config.search defaults', () => {
|
|
|
902
939
|
expect(config.search.fuzzy).toBe(0.2)
|
|
903
940
|
expect(config.search.prefix).toBe(true)
|
|
904
941
|
expect(typeof config.search.boostDocument).toBe('function')
|
|
942
|
+
expect(config.search.query?.boost).toEqual(config.search.boost)
|
|
943
|
+
expect(config.search.query?.fuzzy).toBe(0.2)
|
|
944
|
+
expect(config.search.query?.prefix).toBe(true)
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
it('normalizes legacy search options into query options', () => {
|
|
948
|
+
const config = Config.define({
|
|
949
|
+
search: {
|
|
950
|
+
boost: { text: 10 },
|
|
951
|
+
combineWith: 'OR',
|
|
952
|
+
fuzzy: false,
|
|
953
|
+
prefix: false,
|
|
954
|
+
},
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
expect(config.search.query?.boost).toMatchObject({ title: 4, text: 10 })
|
|
958
|
+
expect(config.search.query?.combineWith).toBe('OR')
|
|
959
|
+
expect(config.search.query?.fuzzy).toBe(false)
|
|
960
|
+
expect(config.search.query?.prefix).toBe(false)
|
|
961
|
+
})
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
describe('SearchConfig', () => {
|
|
965
|
+
it('returns default index options', () => {
|
|
966
|
+
expect(Search.SearchConfig.getIndexOptions(config)).toMatchObject({
|
|
967
|
+
fields: ['category', 'subtitle', 'text', 'title', 'titles'],
|
|
968
|
+
storeFields: [
|
|
969
|
+
'category',
|
|
970
|
+
'href',
|
|
971
|
+
'searchPriority',
|
|
972
|
+
'subtitle',
|
|
973
|
+
'text',
|
|
974
|
+
'title',
|
|
975
|
+
'titles',
|
|
976
|
+
'type',
|
|
977
|
+
],
|
|
978
|
+
})
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
it('keeps required store fields when custom store fields are configured', () => {
|
|
982
|
+
const config = Config.define({
|
|
983
|
+
search: {
|
|
984
|
+
index: {
|
|
985
|
+
storeFields: ['href', 'custom'],
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
expect(Search.SearchConfig.getIndexOptions(config).storeFields).toEqual([
|
|
991
|
+
'category',
|
|
992
|
+
'href',
|
|
993
|
+
'searchPriority',
|
|
994
|
+
'subtitle',
|
|
995
|
+
'text',
|
|
996
|
+
'title',
|
|
997
|
+
'titles',
|
|
998
|
+
'type',
|
|
999
|
+
'custom',
|
|
1000
|
+
])
|
|
905
1001
|
})
|
|
906
1002
|
})
|
|
907
1003
|
|
|
@@ -1006,6 +1102,105 @@ describe('SearchIndex.fromSearchDocuments', () => {
|
|
|
1006
1102
|
expect(results[0]?.score).toBeGreaterThan(results[1]?.score ?? 0)
|
|
1007
1103
|
})
|
|
1008
1104
|
|
|
1105
|
+
it('indexes custom virtual fields', () => {
|
|
1106
|
+
const docs: SearchDocuments.Document[] = [
|
|
1107
|
+
{
|
|
1108
|
+
category: '',
|
|
1109
|
+
href: '/docs/runtime-api',
|
|
1110
|
+
id: '/docs/runtime-api.mdx#runtime-api',
|
|
1111
|
+
searchPriority: undefined,
|
|
1112
|
+
subtitle: '',
|
|
1113
|
+
text: 'Reference content.',
|
|
1114
|
+
title: 'Runtime API',
|
|
1115
|
+
titles: [],
|
|
1116
|
+
type: 'page',
|
|
1117
|
+
},
|
|
1118
|
+
]
|
|
1119
|
+
const config = Config.define({
|
|
1120
|
+
search: {
|
|
1121
|
+
index: {
|
|
1122
|
+
fields: ['path'],
|
|
1123
|
+
extractField(document, fieldName) {
|
|
1124
|
+
if (fieldName === 'path') return document.href
|
|
1125
|
+
return document[fieldName as keyof typeof document]
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
const index = SearchIndex.fromSearchDocuments(docs, config)
|
|
1132
|
+
|
|
1133
|
+
expect(index.search('runtime', Search.SearchConfig.getQueryOptions(config))[0]?.id).toBe(
|
|
1134
|
+
'/docs/runtime-api.mdx#runtime-api',
|
|
1135
|
+
)
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
it('stores custom fields', () => {
|
|
1139
|
+
const docs: SearchDocuments.Document[] = [
|
|
1140
|
+
{
|
|
1141
|
+
category: 'Docs',
|
|
1142
|
+
href: '/guide',
|
|
1143
|
+
id: '/docs/guide.mdx#guide',
|
|
1144
|
+
searchPriority: undefined,
|
|
1145
|
+
subtitle: '',
|
|
1146
|
+
text: 'Install instructions.',
|
|
1147
|
+
title: 'Guide',
|
|
1148
|
+
titles: [],
|
|
1149
|
+
type: 'page',
|
|
1150
|
+
},
|
|
1151
|
+
]
|
|
1152
|
+
const config = Config.define({
|
|
1153
|
+
search: {
|
|
1154
|
+
index: {
|
|
1155
|
+
storeFields: ['path'],
|
|
1156
|
+
extractField(document, fieldName) {
|
|
1157
|
+
if (fieldName === 'path') return document.href
|
|
1158
|
+
return document[fieldName as keyof typeof document]
|
|
1159
|
+
},
|
|
1160
|
+
},
|
|
1161
|
+
},
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
const index = SearchIndex.fromSearchDocuments(docs, config)
|
|
1165
|
+
const result = index.search('guide', Search.SearchConfig.getQueryOptions(config))[0]
|
|
1166
|
+
|
|
1167
|
+
expect(result).toMatchObject({ href: '/guide', path: '/guide', title: 'Guide' })
|
|
1168
|
+
})
|
|
1169
|
+
|
|
1170
|
+
it('applies query fields, weights, and filter at runtime', () => {
|
|
1171
|
+
const docs = [
|
|
1172
|
+
...buildDoc(
|
|
1173
|
+
'/docs/title.mdx',
|
|
1174
|
+
'/title',
|
|
1175
|
+
`# Alpha
|
|
1176
|
+
|
|
1177
|
+
No keyword.
|
|
1178
|
+
`,
|
|
1179
|
+
),
|
|
1180
|
+
...buildDoc(
|
|
1181
|
+
'/docs/text.mdx',
|
|
1182
|
+
'/text',
|
|
1183
|
+
`# Other
|
|
1184
|
+
|
|
1185
|
+
Alpha keyword.
|
|
1186
|
+
`,
|
|
1187
|
+
),
|
|
1188
|
+
]
|
|
1189
|
+
const config = Config.define({
|
|
1190
|
+
search: {
|
|
1191
|
+
query: {
|
|
1192
|
+
fields: ['text'],
|
|
1193
|
+
filter: (result) => result.id !== '/docs/text.mdx#other',
|
|
1194
|
+
weights: { fuzzy: 0.5, prefix: 0.5 },
|
|
1195
|
+
},
|
|
1196
|
+
},
|
|
1197
|
+
})
|
|
1198
|
+
|
|
1199
|
+
const index = SearchIndex.fromSearchDocuments(docs, config)
|
|
1200
|
+
|
|
1201
|
+
expect(index.search('alpha', Search.SearchConfig.getQueryOptions(config))).toEqual([])
|
|
1202
|
+
})
|
|
1203
|
+
|
|
1009
1204
|
it('boosts shallow paths over deep paths', () => {
|
|
1010
1205
|
const shallowDoc: SearchDocuments.Document = {
|
|
1011
1206
|
category: '',
|
package/src/internal/search.ts
CHANGED
|
@@ -14,11 +14,12 @@ import { unified } from 'unified'
|
|
|
14
14
|
import * as UnistUtil from 'unist-util-visit'
|
|
15
15
|
import * as yaml from 'yaml'
|
|
16
16
|
import type * as Config from './config.js'
|
|
17
|
+
import * as MarkdownImports from './markdown-imports.js'
|
|
17
18
|
import { extractSubheading, getPhrasingContentText } from './mdx.js'
|
|
18
19
|
import * as OpenApiRegistry from './openapi/registry.js'
|
|
19
20
|
import * as OpenApiSearch from './openapi/search.js'
|
|
20
21
|
import * as Path from './path.js'
|
|
21
|
-
import {
|
|
22
|
+
import { SearchConfig } from './search.client.js'
|
|
22
23
|
import * as Sidebar from './sidebar.js'
|
|
23
24
|
import * as TaskRunner from './task-runner.js'
|
|
24
25
|
import * as TopNav from './topNav.js'
|
|
@@ -61,7 +62,8 @@ export namespace SearchDocuments {
|
|
|
61
62
|
for (const page of mdxPages)
|
|
62
63
|
taskRunner.run(async () => {
|
|
63
64
|
const filePath = path.join(pagesDirPath, page)
|
|
64
|
-
const
|
|
65
|
+
const rawContent = await fs.promises.readFile(filePath, 'utf-8')
|
|
66
|
+
const content = MarkdownImports.inlineMarkdownImports(rawContent, filePath)
|
|
65
67
|
|
|
66
68
|
const { searchPriority, sections } = extract(content, config)
|
|
67
69
|
if (sections.length === 0) return
|
|
@@ -206,12 +208,11 @@ export namespace SearchIndex {
|
|
|
206
208
|
/**
|
|
207
209
|
* Create a search index from documents.
|
|
208
210
|
*/
|
|
209
|
-
export function fromSearchDocuments(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
})
|
|
211
|
+
export function fromSearchDocuments(
|
|
212
|
+
documents: SearchDocuments.Document[],
|
|
213
|
+
config?: Config.Config,
|
|
214
|
+
): SearchIndex {
|
|
215
|
+
const index = new MiniSearch<SearchDocuments.Document>(SearchConfig.getIndexOptions(config))
|
|
215
216
|
index.addAll(documents)
|
|
216
217
|
return index
|
|
217
218
|
}
|
|
@@ -220,17 +221,15 @@ export namespace SearchIndex {
|
|
|
220
221
|
* Load a search index from a JSON file.
|
|
221
222
|
*/
|
|
222
223
|
export function loadFromFile(options: loadFromFile.Options): SearchIndex {
|
|
223
|
-
const { filePath } = options
|
|
224
|
+
const { config, filePath } = options
|
|
224
225
|
|
|
225
226
|
const json = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
226
|
-
return MiniSearch.loadJSON(json,
|
|
227
|
-
fields: [...searchFields],
|
|
228
|
-
storeFields: [...storeFields],
|
|
229
|
-
})
|
|
227
|
+
return MiniSearch.loadJSON(json, SearchConfig.getIndexOptions(config))
|
|
230
228
|
}
|
|
231
229
|
|
|
232
230
|
export declare namespace loadFromFile {
|
|
233
231
|
type Options = {
|
|
232
|
+
config?: Config.Config
|
|
234
233
|
filePath: string
|
|
235
234
|
}
|
|
236
235
|
}
|
|
@@ -260,7 +259,8 @@ export namespace SearchIndex {
|
|
|
260
259
|
): string[] {
|
|
261
260
|
const { pagesDir, previousIds = [], config } = options
|
|
262
261
|
|
|
263
|
-
const
|
|
262
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8')
|
|
263
|
+
const content = MarkdownImports.inlineMarkdownImports(rawContent, filePath)
|
|
264
264
|
const { searchPriority, sections } = extract(content, config)
|
|
265
265
|
|
|
266
266
|
// Remove old entries for this file
|
|
@@ -636,7 +636,7 @@ export function search(config: Config.Config): PluginOption {
|
|
|
636
636
|
async function buildIndex(): Promise<SearchIndex.SearchIndex> {
|
|
637
637
|
logger.info('Building search index...', { timestamp: true })
|
|
638
638
|
const docs = await SearchDocuments.fromConfig(config)
|
|
639
|
-
const index = SearchIndex.fromSearchDocuments(docs)
|
|
639
|
+
const index = SearchIndex.fromSearchDocuments(docs, config)
|
|
640
640
|
|
|
641
641
|
// Populate fileIds map for HMR
|
|
642
642
|
for (const doc of docs) {
|
package/src/react/Link.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from 'react'
|
|
4
|
-
import {
|
|
3
|
+
import { useContext, useEffect, useState } from 'react'
|
|
4
|
+
import { Link as WakuLink } from 'waku'
|
|
5
|
+
import { unstable_RouterContext as WakuRouterContext } from 'waku/router/client'
|
|
5
6
|
import * as Path from '../internal/path.js'
|
|
6
7
|
|
|
7
8
|
const viewportPrefetchDelayMs = 2_000
|
|
@@ -57,14 +58,18 @@ function useViewportPrefetchReady(enabled: boolean) {
|
|
|
57
58
|
|
|
58
59
|
export function Link(props: Link.Props) {
|
|
59
60
|
const { to, unstable_prefetchOnEnter = true, unstable_prefetchOnView = true, ...rest } = props
|
|
60
|
-
const
|
|
61
|
-
const
|
|
61
|
+
const router = useContext(WakuRouterContext)
|
|
62
|
+
const routerPath = router?.route.path
|
|
63
|
+
const isExternal = Path.isExternal(props.to)
|
|
64
|
+
const prefetchOnView = useViewportPrefetchReady(
|
|
65
|
+
Boolean(unstable_prefetchOnView) && !isExternal && routerPath !== undefined,
|
|
66
|
+
)
|
|
62
67
|
|
|
63
|
-
if (
|
|
64
|
-
return <a {...rest} href={props.to} rel="noopener noreferrer" target="_blank" />
|
|
68
|
+
if (isExternal) return <a {...rest} href={props.to} rel="noopener noreferrer" target="_blank" />
|
|
65
69
|
|
|
66
70
|
const [before, after] = (props.to || '').split('#')
|
|
67
|
-
const resolvedTo = `${before ? before :
|
|
71
|
+
const resolvedTo = `${before ? before : (routerPath ?? '')}${after ? `#${after}` : ''}`
|
|
72
|
+
if (routerPath === undefined) return <a {...rest} href={resolvedTo || props.to} />
|
|
68
73
|
return (
|
|
69
74
|
<WakuLink
|
|
70
75
|
{...rest}
|
|
@@ -11,7 +11,7 @@ import LucideFile from '~icons/lucide/file'
|
|
|
11
11
|
import LucideHash from '~icons/lucide/hash'
|
|
12
12
|
import LucideSearch from '~icons/lucide/search'
|
|
13
13
|
import * as Path from '../../internal/path.js'
|
|
14
|
-
import {
|
|
14
|
+
import { SearchConfig } from '../../internal/search.client.js'
|
|
15
15
|
import { Link } from '../Link.js'
|
|
16
16
|
import { useConfig } from '../useConfig.js'
|
|
17
17
|
import { DialogTrigger } from './DialogTrigger.js'
|
|
@@ -86,14 +86,12 @@ export function Search(props: Search.Props) {
|
|
|
86
86
|
const json = await getSearchIndex()
|
|
87
87
|
setIndex(
|
|
88
88
|
MiniSearch.loadJSON<SearchResult>(json, {
|
|
89
|
-
|
|
90
|
-
storeFields: [...storeFields],
|
|
91
|
-
tokenize,
|
|
89
|
+
...SearchConfig.getIndexOptions(config),
|
|
92
90
|
}),
|
|
93
91
|
)
|
|
94
92
|
})
|
|
95
93
|
.catch((error) => console.error('Failed to load search index:', error))
|
|
96
|
-
}, [open, index])
|
|
94
|
+
}, [open, index, config])
|
|
97
95
|
|
|
98
96
|
React.useEffect(() => {
|
|
99
97
|
try {
|
|
@@ -108,12 +106,11 @@ export function Search(props: Search.Props) {
|
|
|
108
106
|
return
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
const results = (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
109
|
+
const results = (
|
|
110
|
+
index.search(query, SearchConfig.getQueryOptions(config)) as SearchResult[]
|
|
111
|
+
).slice(0, 20)
|
|
115
112
|
setSearch((s) => ({ ...s, results, selectedIndex: 0 }))
|
|
116
|
-
}, [query, index, config
|
|
113
|
+
}, [query, index, config])
|
|
117
114
|
|
|
118
115
|
React.useEffect(() => {
|
|
119
116
|
if (disableKeyboardShortcut) return
|