@kirchdev/duxt 0.1.1-dev.14-636388d → 0.1.1-dev.16-c752db1
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/app/composables/useDuxtSearch.ts +89 -43
- package/app/utils/search-scope.ts +41 -0
- package/package.json +1 -1
- package/sections-resolve.ts +2 -0
- package/sources-resolve.ts +39 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { DuxtResolvedSource } from '../../sources-resolve';
|
|
2
|
+
import { searchSources } from '../utils/search-scope';
|
|
1
3
|
import type { DuxtSearchSection } from '@duxt/composables/useFuzzySearch';
|
|
2
4
|
|
|
3
5
|
/** A hit, with the source it came out of. */
|
|
@@ -22,18 +24,19 @@ export interface DuxtSearchHit extends DuxtSearchSection {
|
|
|
22
24
|
* The source being read goes first in each round, because a reader searching
|
|
23
25
|
* inside a project usually means that project.
|
|
24
26
|
*
|
|
25
|
-
* ONE VERSION PER
|
|
27
|
+
* ONE VERSION PER ARTEFACT. Searching every version returns each page as many
|
|
26
28
|
* times as there are versions, which buries the answer under its own history.
|
|
27
|
-
*
|
|
29
|
+
* Each docs tree and generated declaration contributes its current or default edition.
|
|
28
30
|
*/
|
|
29
31
|
export function useDuxtSearch() {
|
|
30
32
|
const duxt = useDuxtConfig();
|
|
31
33
|
const { source } = useDuxtCollection();
|
|
34
|
+
const { locale, fallbackLocale } = useI18n();
|
|
32
35
|
|
|
33
36
|
const sources = computed(() => duxt.resolvedSources ?? []);
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
|
-
* The collections
|
|
39
|
+
* The available collections; only selected editions are initialized.
|
|
37
40
|
*
|
|
38
41
|
* Fixed at setup, deliberately: each one needs its own `useSearchCollection`,
|
|
39
42
|
* and a composable list that grows with the route is not a thing. The
|
|
@@ -79,38 +82,54 @@ export function useDuxtSearch() {
|
|
|
79
82
|
}).search
|
|
80
83
|
}));
|
|
81
84
|
|
|
82
|
-
/**
|
|
83
|
-
const active = computed(() =>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
/** Selected editions, ordered by artefact and then preferred language. */
|
|
86
|
+
const active = computed(() =>
|
|
87
|
+
searchSources(
|
|
88
|
+
searchable,
|
|
89
|
+
source.value,
|
|
90
|
+
locale.value,
|
|
91
|
+
fallbackLocale.value as string | string[] | undefined
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const pagePaths = new Map<string, Promise<Set<string>>>();
|
|
96
|
+
function paths(collection: string) {
|
|
97
|
+
let pending = pagePaths.get(collection);
|
|
98
|
+
if (!pending) {
|
|
99
|
+
pending = queryCollection(collection as DuxtCollectionArg)
|
|
100
|
+
.select('path')
|
|
101
|
+
.all()
|
|
102
|
+
.then((pages) => new Set(pages.map((page) => page.path)))
|
|
103
|
+
.catch((error) => {
|
|
104
|
+
pagePaths.delete(collection);
|
|
105
|
+
throw error;
|
|
106
|
+
});
|
|
107
|
+
pagePaths.set(collection, pending);
|
|
100
108
|
}
|
|
109
|
+
return pending;
|
|
110
|
+
}
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
// A fallback may answer only for a PAGE absent from every better language,
|
|
113
|
+
// even when a heading or the search term exists only in the original.
|
|
114
|
+
async function visiblePages(wanted: DuxtResolvedSource[]) {
|
|
115
|
+
return Promise.all(
|
|
116
|
+
wanted.map(async (entry, index) => {
|
|
117
|
+
const preferred = wanted
|
|
118
|
+
.slice(0, index)
|
|
119
|
+
.filter((other) => other.prefix === entry.prefix);
|
|
120
|
+
return new Set(
|
|
121
|
+
(
|
|
122
|
+
await Promise.all(preferred.map((other) => paths(other.collection)))
|
|
123
|
+
).flatMap((pages) => [...pages])
|
|
124
|
+
);
|
|
125
|
+
})
|
|
109
126
|
);
|
|
110
|
-
}
|
|
127
|
+
}
|
|
111
128
|
|
|
112
129
|
/** Only worth a badge when there is more than one thing to tell apart. */
|
|
113
|
-
const labelled = computed(
|
|
130
|
+
const labelled = computed(
|
|
131
|
+
() => new Set(active.value.map((entry) => entry.prefix)).size > 1
|
|
132
|
+
);
|
|
114
133
|
|
|
115
134
|
const labelOf = (entry: (typeof searchable)[number]) =>
|
|
116
135
|
[entry.repo, entry.version].filter(Boolean).join(' ') ||
|
|
@@ -119,8 +138,8 @@ export function useDuxtSearch() {
|
|
|
119
138
|
|
|
120
139
|
async function init() {
|
|
121
140
|
await Promise.all(
|
|
122
|
-
|
|
123
|
-
.
|
|
141
|
+
active.value
|
|
142
|
+
.map((entry) => searches.find((search) => search.entry === entry)!)
|
|
124
143
|
.filter((search) => search.status.value === 'idle')
|
|
125
144
|
.map((search) => search.init())
|
|
126
145
|
);
|
|
@@ -153,17 +172,41 @@ export function useDuxtSearch() {
|
|
|
153
172
|
term: string,
|
|
154
173
|
limit = 20
|
|
155
174
|
): Promise<{ hits: DuxtSearchHit[]; approximate: boolean }> {
|
|
156
|
-
if (!term.trim()) return { hits: [], approximate: false };
|
|
175
|
+
if (!term.trim() || limit <= 0) return { hits: [], approximate: false };
|
|
157
176
|
|
|
158
177
|
const wanted = active.value;
|
|
178
|
+
const hidden = await visiblePages(wanted);
|
|
179
|
+
async function visible(
|
|
180
|
+
query: (requested: number) => Promise<DuxtSearchSection[]>,
|
|
181
|
+
index: number
|
|
182
|
+
) {
|
|
183
|
+
let requested = limit;
|
|
184
|
+
while (true) {
|
|
185
|
+
const hits = await query(requested);
|
|
186
|
+
const shown = hits.filter(
|
|
187
|
+
(hit) => !hidden[index]!.has(hit.id.split('#')[0]!)
|
|
188
|
+
);
|
|
189
|
+
if (
|
|
190
|
+
shown.length >= limit ||
|
|
191
|
+
hits.length < requested ||
|
|
192
|
+
!hidden[index]!.size
|
|
193
|
+
)
|
|
194
|
+
return shown.slice(0, limit);
|
|
195
|
+
requested *= 2;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
159
198
|
|
|
160
199
|
const lists = await Promise.all(
|
|
161
|
-
|
|
162
|
-
.
|
|
163
|
-
.map(async (entry) => {
|
|
164
|
-
const hits =
|
|
165
|
-
|
|
166
|
-
|
|
200
|
+
wanted
|
|
201
|
+
.map((source) => searches.find((entry) => entry.entry === source)!)
|
|
202
|
+
.map(async (entry, index) => {
|
|
203
|
+
const hits = await visible(
|
|
204
|
+
async (requested) =>
|
|
205
|
+
(await entry.search(term, {
|
|
206
|
+
limit: requested
|
|
207
|
+
})) as DuxtSearchSection[],
|
|
208
|
+
index
|
|
209
|
+
);
|
|
167
210
|
|
|
168
211
|
return hits.map((hit) => ({
|
|
169
212
|
...hit,
|
|
@@ -184,10 +227,13 @@ export function useDuxtSearch() {
|
|
|
184
227
|
// leaves the reader with an empty box. Fuse gets a second look, over the
|
|
185
228
|
// same sources and merged the same way.
|
|
186
229
|
const fuzzy = await Promise.all(
|
|
187
|
-
|
|
188
|
-
.
|
|
189
|
-
.map(async (entry) => {
|
|
190
|
-
const hits = await
|
|
230
|
+
wanted
|
|
231
|
+
.map((source) => approximate.find((entry) => entry.entry === source)!)
|
|
232
|
+
.map(async (entry, index) => {
|
|
233
|
+
const hits = await visible(
|
|
234
|
+
(requested) => entry.search(term, requested),
|
|
235
|
+
index
|
|
236
|
+
);
|
|
191
237
|
|
|
192
238
|
return hits.map((hit) => ({
|
|
193
239
|
...hit,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sourcesForRoute,
|
|
3
|
+
type DuxtResolvedSource
|
|
4
|
+
} from '../../sources-resolve';
|
|
5
|
+
|
|
6
|
+
/** Select the editions participating in site-wide search. */
|
|
7
|
+
export function searchSources(
|
|
8
|
+
sources: DuxtResolvedSource[],
|
|
9
|
+
current: DuxtResolvedSource | undefined,
|
|
10
|
+
locale?: string,
|
|
11
|
+
fallbackLocale?: string | string[]
|
|
12
|
+
): DuxtResolvedSource[] {
|
|
13
|
+
const byArtefact = new Map<string, DuxtResolvedSource>();
|
|
14
|
+
for (const entry of sources) {
|
|
15
|
+
const key = JSON.stringify([entry.repo, entry.generated?.declaration]);
|
|
16
|
+
const chosen = byArtefact.get(key);
|
|
17
|
+
if (entry.collection === current?.collection) {
|
|
18
|
+
byArtefact.set(key, entry);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (chosen?.collection === current?.collection) continue;
|
|
22
|
+
if (!chosen || (entry.isDefault && !chosen.isDefault))
|
|
23
|
+
byArtefact.set(key, entry);
|
|
24
|
+
}
|
|
25
|
+
return [...byArtefact.values()]
|
|
26
|
+
.sort((a, b) =>
|
|
27
|
+
a.collection === current?.collection
|
|
28
|
+
? -1
|
|
29
|
+
: b.collection === current?.collection
|
|
30
|
+
? 1
|
|
31
|
+
: 0
|
|
32
|
+
)
|
|
33
|
+
.flatMap((edition) =>
|
|
34
|
+
sourcesForRoute(
|
|
35
|
+
edition.prefix,
|
|
36
|
+
locale ?? current?.locale,
|
|
37
|
+
sources.filter((entry) => entry.prefix === edition.prefix),
|
|
38
|
+
fallbackLocale
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "@kirchdev/duxt",
|
|
4
|
-
"version": "0.1.1-dev.
|
|
4
|
+
"version": "0.1.1-dev.16-c752db1",
|
|
5
5
|
"description": "Nuxt documentation layer on Content v3 — multi-repo, versioned docs from one compact source list.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"content-v3",
|
package/sections-resolve.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
DuxtSourcesOptions
|
|
23
23
|
} from './sources-resolve';
|
|
24
24
|
import {
|
|
25
|
+
assertCollectionIdentities,
|
|
25
26
|
expandSources,
|
|
26
27
|
identifier,
|
|
27
28
|
resolveSources,
|
|
@@ -539,6 +540,7 @@ export function resolveGeneratedSections(
|
|
|
539
540
|
}
|
|
540
541
|
});
|
|
541
542
|
|
|
543
|
+
assertCollectionIdentities([...resolved, ...generated]);
|
|
542
544
|
return generated;
|
|
543
545
|
}
|
|
544
546
|
|
package/sources-resolve.ts
CHANGED
|
@@ -631,6 +631,7 @@ export function resolveSources(
|
|
|
631
631
|
});
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
+
assertCollectionIdentities(resolved);
|
|
634
635
|
return resolved;
|
|
635
636
|
}
|
|
636
637
|
|
|
@@ -906,3 +907,41 @@ function isInsidePrefix(path: string, prefix: string): boolean {
|
|
|
906
907
|
|
|
907
908
|
return path === prefix || path.startsWith(`${prefix}/`);
|
|
908
909
|
}
|
|
910
|
+
|
|
911
|
+
/** Validate names before any collection map can overwrite a declaration. */
|
|
912
|
+
export function assertCollectionIdentities(
|
|
913
|
+
entries: DuxtResolvedSource[]
|
|
914
|
+
): void {
|
|
915
|
+
const taken = new Map<string, string>();
|
|
916
|
+
const claim = (name: string, declaration: string) => {
|
|
917
|
+
const previous = taken.get(name);
|
|
918
|
+
if (previous !== undefined) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`duxt: collection "${name}" is claimed by both ${previous} and ${declaration}. ` +
|
|
921
|
+
'Give one declaration a distinct `slug` or `label`.'
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
taken.set(name, declaration);
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
for (const entry of entries) {
|
|
928
|
+
const kind = entry.generated
|
|
929
|
+
? `generated section "${entry.generated.label}"`
|
|
930
|
+
: 'documentation';
|
|
931
|
+
claim(
|
|
932
|
+
entry.collection,
|
|
933
|
+
`${kind} "${entry.path}" at "${entry.prefix || '/'}"` +
|
|
934
|
+
(entry.locale ? ` (locale "${entry.locale}")` : '')
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Shared partials are one declaration per language, even across sources.
|
|
939
|
+
const locales = new Set(
|
|
940
|
+
entries
|
|
941
|
+
.filter((entry) => !entry.generated)
|
|
942
|
+
.map((entry) => (entry.isDefaultLocale ? undefined : entry.locale))
|
|
943
|
+
);
|
|
944
|
+
for (const locale of locales) {
|
|
945
|
+
claim(partialsCollection(locale), `partials for "${locale ?? 'default'}"`);
|
|
946
|
+
}
|
|
947
|
+
}
|