astro-accelerator 4.1.6 → 4.1.8
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/package.json +1 -1
- package/public/css/main.css +9 -19
- package/public/js/search.js +47 -9
- package/public/js/search2.js +676 -0
- package/public/js/synonyms.js +14 -0
- package/src/config.ts +1 -1
package/package.json
CHANGED
package/public/css/main.css
CHANGED
|
@@ -774,7 +774,8 @@ nav.site-nav h2 {
|
|
|
774
774
|
background-color: initial;
|
|
775
775
|
}
|
|
776
776
|
|
|
777
|
-
.result-wrapper mark
|
|
777
|
+
.result-wrapper mark,
|
|
778
|
+
.result-headings mark {
|
|
778
779
|
background-color: var(--aft-mark);
|
|
779
780
|
}
|
|
780
781
|
|
|
@@ -874,26 +875,15 @@ nav.site-nav h2 {
|
|
|
874
875
|
cursor: pointer;
|
|
875
876
|
}
|
|
876
877
|
|
|
877
|
-
.result-headings
|
|
878
|
-
display:
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
.result-headings li:nth-child(-n + 3) {
|
|
882
|
-
display: block;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
.result-headings:has(li:nth-child(n + 4))::after {
|
|
886
|
-
content: 'See more >';
|
|
887
|
-
color: var(--fore-link);
|
|
888
|
-
text-decoration: underline;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
.result-headings:focus-within li {
|
|
892
|
-
display: block;
|
|
878
|
+
.result-headings {
|
|
879
|
+
display: grid;
|
|
880
|
+
grid-template-columns: 1fr 1fr;
|
|
893
881
|
}
|
|
894
882
|
|
|
895
|
-
.result-headings
|
|
896
|
-
|
|
883
|
+
.result-headings li {
|
|
884
|
+
text-align: left;
|
|
885
|
+
padding: 0.1rem 3.6rem;
|
|
886
|
+
font-size: 1rem;
|
|
897
887
|
}
|
|
898
888
|
|
|
899
889
|
.show-more {
|
package/public/js/search.js
CHANGED
|
@@ -26,6 +26,17 @@ function enabled(settings, option) {
|
|
|
26
26
|
return settings && settings.includes(option);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
*
|
|
31
|
+
* @param {any} value
|
|
32
|
+
* @param {number} index
|
|
33
|
+
* @param {any[]} array
|
|
34
|
+
* @returns
|
|
35
|
+
*/
|
|
36
|
+
function unique(value, index, array) {
|
|
37
|
+
return array.indexOf(value) === index;
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
/**
|
|
30
41
|
@typedef {
|
|
31
42
|
{
|
|
@@ -38,6 +49,7 @@ function enabled(settings, option) {
|
|
|
38
49
|
@typedef {
|
|
39
50
|
{
|
|
40
51
|
foundWords: number;
|
|
52
|
+
foundTerms: string[];
|
|
41
53
|
score: number;
|
|
42
54
|
depth: number;
|
|
43
55
|
title: string;
|
|
@@ -72,6 +84,7 @@ function initializeSearch() {
|
|
|
72
84
|
var currentQuery = '';
|
|
73
85
|
var dataUrl = siteSearchElement.dataset.sourcedata;
|
|
74
86
|
|
|
87
|
+
// Legacy scoring
|
|
75
88
|
var scoring = {
|
|
76
89
|
depth: 5,
|
|
77
90
|
phraseTitle: 60,
|
|
@@ -84,6 +97,14 @@ function initializeSearch() {
|
|
|
84
97
|
termKeywords: 15,
|
|
85
98
|
};
|
|
86
99
|
|
|
100
|
+
// Found word scoring
|
|
101
|
+
const scores = {
|
|
102
|
+
titleExact: 20,
|
|
103
|
+
titleContains: 15,
|
|
104
|
+
headingContains: 10,
|
|
105
|
+
contentContains: 1,
|
|
106
|
+
};
|
|
107
|
+
|
|
87
108
|
var ready = false;
|
|
88
109
|
var scrolled = false;
|
|
89
110
|
|
|
@@ -246,6 +267,7 @@ function initializeSearch() {
|
|
|
246
267
|
/**
|
|
247
268
|
* Replaces synonyms
|
|
248
269
|
* @param {string[]} queryTerms
|
|
270
|
+
* @returns {Promise<string[]>}
|
|
249
271
|
*/
|
|
250
272
|
async function replaceSynonyms(queryTerms) {
|
|
251
273
|
const synonyms = await getSynonyms();
|
|
@@ -253,11 +275,16 @@ function initializeSearch() {
|
|
|
253
275
|
for (let i = 0; i < queryTerms.length; i++) {
|
|
254
276
|
const term = queryTerms[i];
|
|
255
277
|
if (synonyms[term] != null) {
|
|
256
|
-
|
|
278
|
+
if (synonyms[term].length === 0) {
|
|
279
|
+
// @ts-ignore
|
|
280
|
+
queryTerms[i] = null;
|
|
281
|
+
} else {
|
|
282
|
+
queryTerms.push(synonyms[term]);
|
|
283
|
+
}
|
|
257
284
|
}
|
|
258
285
|
}
|
|
259
286
|
|
|
260
|
-
return queryTerms;
|
|
287
|
+
return queryTerms.filter((qt) => qt != null);
|
|
261
288
|
}
|
|
262
289
|
|
|
263
290
|
/**
|
|
@@ -305,6 +332,7 @@ function initializeSearch() {
|
|
|
305
332
|
cleanQuery.length > 0 &&
|
|
306
333
|
haystack.forEach((item) => {
|
|
307
334
|
item.foundWords = 0;
|
|
335
|
+
item.foundTerms = [];
|
|
308
336
|
item.score = 0;
|
|
309
337
|
item.matchedHeadings = [];
|
|
310
338
|
|
|
@@ -314,12 +342,12 @@ function initializeSearch() {
|
|
|
314
342
|
|
|
315
343
|
// Title
|
|
316
344
|
if (item.safeTitle === currentQuery) {
|
|
317
|
-
item.foundWords +=
|
|
345
|
+
item.foundWords += scores.titleExact;
|
|
318
346
|
}
|
|
319
347
|
|
|
320
348
|
if (contains(item.safeTitle, currentQuery)) {
|
|
321
349
|
item.score = item.score + scoring.phraseTitle;
|
|
322
|
-
item.foundWords +=
|
|
350
|
+
item.foundWords += scores.titleContains;
|
|
323
351
|
}
|
|
324
352
|
|
|
325
353
|
// Headings
|
|
@@ -327,19 +355,21 @@ function initializeSearch() {
|
|
|
327
355
|
if (contains(c.safeText, currentQuery)) {
|
|
328
356
|
item.score = item.score + scoring.phraseHeading;
|
|
329
357
|
item.matchedHeadings.push(c);
|
|
330
|
-
item.foundWords
|
|
358
|
+
item.foundWords += scores.headingContains;
|
|
331
359
|
}
|
|
332
360
|
});
|
|
333
361
|
|
|
334
362
|
// Description
|
|
335
363
|
if (contains(item.description, currentQuery)) {
|
|
336
364
|
item.score = item.score + scoring.phraseDescription;
|
|
337
|
-
item.foundWords
|
|
365
|
+
item.foundWords += scores.contentContains;
|
|
338
366
|
}
|
|
339
367
|
|
|
340
368
|
// Part 2 - Term Matches, i.e. "Kitchen" or "Sink"
|
|
341
369
|
|
|
342
370
|
let foundWords = 0;
|
|
371
|
+
/** @type{string[]} */
|
|
372
|
+
let foundTerms = [];
|
|
343
373
|
|
|
344
374
|
allTerms.forEach((term) => {
|
|
345
375
|
let isTermFound = false;
|
|
@@ -388,10 +418,14 @@ function initializeSearch() {
|
|
|
388
418
|
|
|
389
419
|
if (isTermFound) {
|
|
390
420
|
foundWords++;
|
|
421
|
+
foundTerms.push(term);
|
|
391
422
|
}
|
|
392
423
|
});
|
|
393
424
|
|
|
394
425
|
item.foundWords += foundWords;
|
|
426
|
+
item.foundTerms = item.foundTerms
|
|
427
|
+
.concat(foundTerms)
|
|
428
|
+
.filter(unique);
|
|
395
429
|
|
|
396
430
|
if (item.score > 0) {
|
|
397
431
|
needles.push(item);
|
|
@@ -413,11 +447,15 @@ function initializeSearch() {
|
|
|
413
447
|
});
|
|
414
448
|
|
|
415
449
|
needles.sort(function (a, b) {
|
|
416
|
-
if (b.
|
|
417
|
-
|
|
450
|
+
if (b.foundTerms.length === a.foundTerms.length) {
|
|
451
|
+
if (b.foundWords === a.foundWords) {
|
|
452
|
+
return b.score - a.score;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return b.foundWords - a.foundWords;
|
|
418
456
|
}
|
|
419
457
|
|
|
420
|
-
return b.
|
|
458
|
+
return b.foundTerms.length - a.foundTerms.length;
|
|
421
459
|
});
|
|
422
460
|
|
|
423
461
|
const total = needles.reduce(function (accumulator, needle) {
|
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This javascript file comes from Astro Accelerator
|
|
3
|
+
* Edits will be overwritten if you change the file locally
|
|
4
|
+
* THIS IS THE PREVIOUS SEARCH VERSION
|
|
5
|
+
*
|
|
6
|
+
* @format
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// @ts-check
|
|
10
|
+
|
|
11
|
+
import { qs } from './modules/query.js';
|
|
12
|
+
import { raiseEvent } from './modules/events.js';
|
|
13
|
+
import { removeScroll, resetScroll } from './modules/scrollbar.js';
|
|
14
|
+
import { contains, sanitise, explode, highlight } from './modules/string.js';
|
|
15
|
+
import { stemmer } from './modules/stemmer.js';
|
|
16
|
+
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
const f = window.site_features ?? {};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
*
|
|
22
|
+
* @param {string[]} settings
|
|
23
|
+
* @param {string} option
|
|
24
|
+
* @returns
|
|
25
|
+
*/
|
|
26
|
+
function enabled(settings, option) {
|
|
27
|
+
return settings && settings.includes(option);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
@typedef {
|
|
32
|
+
{
|
|
33
|
+
text: string;
|
|
34
|
+
safeText: string;
|
|
35
|
+
slug: string;
|
|
36
|
+
}
|
|
37
|
+
} Heading
|
|
38
|
+
|
|
39
|
+
@typedef {
|
|
40
|
+
{
|
|
41
|
+
foundWords: number;
|
|
42
|
+
score: number;
|
|
43
|
+
depth: number;
|
|
44
|
+
title: string;
|
|
45
|
+
keywords: string;
|
|
46
|
+
safeTitle: string;
|
|
47
|
+
description: string;
|
|
48
|
+
safeDescription: string;
|
|
49
|
+
headings: Heading[];
|
|
50
|
+
tags: string[];
|
|
51
|
+
url: string;
|
|
52
|
+
date: string;
|
|
53
|
+
matchedHeadings: Heading[];
|
|
54
|
+
}
|
|
55
|
+
} SearchEntry
|
|
56
|
+
|
|
57
|
+
@typedef {
|
|
58
|
+
{
|
|
59
|
+
[ix: string]: string
|
|
60
|
+
}
|
|
61
|
+
} Synonyms
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
function initializeSearch() {
|
|
65
|
+
const siteSearchInput = qs('[data-site-search-query]');
|
|
66
|
+
const siteSearchWrapper = qs('[data-site-search-wrapper]');
|
|
67
|
+
const siteSearchElement = qs('[data-site-search]');
|
|
68
|
+
const siteSearchResults = qs('[data-site-search-results');
|
|
69
|
+
const removeSearchButton = qs('[data-site-search-remove]');
|
|
70
|
+
|
|
71
|
+
/** @type {SearchEntry[]} */
|
|
72
|
+
var haystack = [];
|
|
73
|
+
var currentQuery = '';
|
|
74
|
+
var dataUrl = siteSearchElement.dataset.sourcedata;
|
|
75
|
+
|
|
76
|
+
var scoring = {
|
|
77
|
+
depth: 5,
|
|
78
|
+
phraseTitle: 60,
|
|
79
|
+
phraseHeading: 20,
|
|
80
|
+
phraseDescription: 20,
|
|
81
|
+
termTitle: 40,
|
|
82
|
+
termHeading: 15,
|
|
83
|
+
termDescription: 15,
|
|
84
|
+
termTags: 15,
|
|
85
|
+
termKeywords: 15,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
var ready = false;
|
|
89
|
+
var scrolled = false;
|
|
90
|
+
|
|
91
|
+
siteSearchInput.addEventListener('focus', () => activateInput());
|
|
92
|
+
|
|
93
|
+
function deactivate(e) {
|
|
94
|
+
if (
|
|
95
|
+
!siteSearchElement.contains(e.target) &&
|
|
96
|
+
!siteSearchResults.contains(e.target)
|
|
97
|
+
) {
|
|
98
|
+
closeDropdown();
|
|
99
|
+
|
|
100
|
+
const duration = getComputedStyle(
|
|
101
|
+
siteSearchWrapper
|
|
102
|
+
).getPropertyValue('--search-dropdown-duration');
|
|
103
|
+
|
|
104
|
+
// Convert duration to milliseconds for setTimeout
|
|
105
|
+
const durationMs =
|
|
106
|
+
parseFloat(duration) * (duration.endsWith('ms') ? 1 : 1000);
|
|
107
|
+
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
deactivateInput();
|
|
110
|
+
}, durationMs);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Close the dropdown upon activity outside the search
|
|
115
|
+
document.addEventListener('click', deactivate);
|
|
116
|
+
document.addEventListener('keydown', deactivate);
|
|
117
|
+
|
|
118
|
+
function removeSearch() {
|
|
119
|
+
clearInput();
|
|
120
|
+
deactivateInput();
|
|
121
|
+
debounceSearch();
|
|
122
|
+
siteSearchInput.focus();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Clear the search input
|
|
126
|
+
removeSearchButton.addEventListener('click', () => {
|
|
127
|
+
removeSearch();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Dropdown accessibility controls
|
|
131
|
+
document.addEventListener('keydown', handleDropdownKeyboardNavigation);
|
|
132
|
+
|
|
133
|
+
function activateInput() {
|
|
134
|
+
if (
|
|
135
|
+
siteSearchWrapper.classList.contains('is-active') ||
|
|
136
|
+
siteSearchInput.value.trim().length === 0
|
|
137
|
+
) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
siteSearchWrapper.classList.add('is-active');
|
|
141
|
+
removeScroll();
|
|
142
|
+
openDropdown();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function deactivateInput() {
|
|
146
|
+
if (!siteSearchWrapper.classList.contains('is-active')) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
siteSearchWrapper.classList.remove('is-active');
|
|
150
|
+
resetScroll();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function openDropdown() {
|
|
154
|
+
siteSearchElement.classList.add('is-active');
|
|
155
|
+
|
|
156
|
+
requestAnimationFrame(() => {
|
|
157
|
+
const dropdownHeightPercentage = parseFloat(
|
|
158
|
+
getComputedStyle(siteSearchWrapper).getPropertyValue(
|
|
159
|
+
'--search-dropdown-height'
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
// Convert vh to pixels
|
|
163
|
+
const dropdownHeight =
|
|
164
|
+
window.innerHeight * (dropdownHeightPercentage / 100) + 32;
|
|
165
|
+
const siteSearchElementRect =
|
|
166
|
+
siteSearchElement.getBoundingClientRect();
|
|
167
|
+
const offsetFromBottomToElement =
|
|
168
|
+
window.innerHeight - siteSearchElementRect.bottom;
|
|
169
|
+
|
|
170
|
+
if (offsetFromBottomToElement < dropdownHeight) {
|
|
171
|
+
document.body.style.overflow = '';
|
|
172
|
+
|
|
173
|
+
// Scroll to the siteSearchElement
|
|
174
|
+
siteSearchElement.scrollIntoView({
|
|
175
|
+
behavior: 'smooth',
|
|
176
|
+
block: 'start',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Delay the overflow to allow for smooth scrolling
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
document.body.style.overflow = 'hidden';
|
|
182
|
+
}, 300);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function closeDropdown() {
|
|
188
|
+
siteSearchElement.classList.remove('is-active');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function clearInput() {
|
|
192
|
+
closeDropdown();
|
|
193
|
+
siteSearchInput.value = '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleDropdownKeyboardNavigation(e) {
|
|
197
|
+
// Proceed only if search dropdown is active
|
|
198
|
+
if (!siteSearchWrapper.classList.contains('is-active')) return;
|
|
199
|
+
|
|
200
|
+
if (e.key === 'Escape') {
|
|
201
|
+
removeSearch();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (e.key === 'Tab') {
|
|
206
|
+
const firstElement = siteSearchInput;
|
|
207
|
+
const lastElement =
|
|
208
|
+
siteSearchResults.querySelector('button') ||
|
|
209
|
+
siteSearchResults.querySelector(
|
|
210
|
+
'.site-search-results-item:last-child .result-wrapper'
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (e.shiftKey && document.activeElement === firstElement) {
|
|
214
|
+
// Shift + Tab: Move focus to the last element if the first element is currently focused
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
if (lastElement) lastElement.focus();
|
|
217
|
+
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
|
218
|
+
// Tab: Move focus to the first element if the last element is currently focused
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
firstElement.focus();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @type{Synonyms | null} */
|
|
226
|
+
var _synonyms = null;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Gets the list of synonyms if they exist
|
|
230
|
+
* @returns { Promise<Synonyms> }
|
|
231
|
+
*/
|
|
232
|
+
async function getSynonyms() {
|
|
233
|
+
if (_synonyms != null) {
|
|
234
|
+
return _synonyms;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const synonymsModule = await import('./synonyms.js');
|
|
239
|
+
_synonyms = synonymsModule.synonyms;
|
|
240
|
+
} catch {
|
|
241
|
+
_synonyms = {};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return _synonyms ?? {};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Replaces synonyms
|
|
249
|
+
* @param {string[]} queryTerms
|
|
250
|
+
*/
|
|
251
|
+
async function replaceSynonyms(queryTerms) {
|
|
252
|
+
const synonyms = await getSynonyms();
|
|
253
|
+
|
|
254
|
+
for (let i = 0; i < queryTerms.length; i++) {
|
|
255
|
+
const term = queryTerms[i];
|
|
256
|
+
if (synonyms[term] != null) {
|
|
257
|
+
queryTerms.push(synonyms[term]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return queryTerms;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Search term `s` and number of results `r`
|
|
266
|
+
* @param {string} s
|
|
267
|
+
* @param {number|null} [r=12]
|
|
268
|
+
* @returns
|
|
269
|
+
*/
|
|
270
|
+
async function search(s, r) {
|
|
271
|
+
const numberOfResults = r ?? 12;
|
|
272
|
+
|
|
273
|
+
// Add 'is-active' class when search is performed
|
|
274
|
+
if (s && s.trim().length > 0) {
|
|
275
|
+
activateInput();
|
|
276
|
+
openDropdown();
|
|
277
|
+
} else if (siteSearchElement.classList.contains('is-active')) {
|
|
278
|
+
// Remove 'is-active' class when search is cleared
|
|
279
|
+
closeDropdown();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** @type {SearchEntry[]} */
|
|
283
|
+
const needles = [];
|
|
284
|
+
|
|
285
|
+
// Clean the input
|
|
286
|
+
const cleanQuery = sanitise(s);
|
|
287
|
+
|
|
288
|
+
if (currentQuery === cleanQuery) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
currentQuery = cleanQuery;
|
|
293
|
+
/** @type {string[]} */
|
|
294
|
+
const stemmedTerms = [];
|
|
295
|
+
const queryTerms = await replaceSynonyms(explode(currentQuery));
|
|
296
|
+
|
|
297
|
+
for (const term of queryTerms) {
|
|
298
|
+
const stemmed = stemmer(term);
|
|
299
|
+
if (stemmed !== term) {
|
|
300
|
+
stemmedTerms.push(stemmed);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const allTerms = queryTerms.concat(stemmedTerms);
|
|
305
|
+
|
|
306
|
+
cleanQuery.length > 0 &&
|
|
307
|
+
haystack.forEach((item) => {
|
|
308
|
+
item.foundWords = 0;
|
|
309
|
+
item.score = 0;
|
|
310
|
+
item.matchedHeadings = [];
|
|
311
|
+
|
|
312
|
+
// The user searches for "Kitchen Sink"
|
|
313
|
+
|
|
314
|
+
// Part 1 - Phrase Matches, i.e. "Kitchen Sink"
|
|
315
|
+
|
|
316
|
+
// Title
|
|
317
|
+
if (item.safeTitle === currentQuery) {
|
|
318
|
+
item.foundWords += 2;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (contains(item.safeTitle, currentQuery)) {
|
|
322
|
+
item.score = item.score + scoring.phraseTitle;
|
|
323
|
+
item.foundWords += 2;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Headings
|
|
327
|
+
item.headings.forEach((c) => {
|
|
328
|
+
if (contains(c.safeText, currentQuery)) {
|
|
329
|
+
item.score = item.score + scoring.phraseHeading;
|
|
330
|
+
item.matchedHeadings.push(c);
|
|
331
|
+
item.foundWords++;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Description
|
|
336
|
+
if (contains(item.description, currentQuery)) {
|
|
337
|
+
item.score = item.score + scoring.phraseDescription;
|
|
338
|
+
item.foundWords++;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Part 2 - Term Matches, i.e. "Kitchen" or "Sink"
|
|
342
|
+
|
|
343
|
+
let foundWords = 0;
|
|
344
|
+
|
|
345
|
+
allTerms.forEach((term) => {
|
|
346
|
+
let isTermFound = false;
|
|
347
|
+
|
|
348
|
+
// Title
|
|
349
|
+
if (contains(item.safeTitle, term)) {
|
|
350
|
+
item.score = item.score + scoring.termTitle;
|
|
351
|
+
isTermFound = true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Headings
|
|
355
|
+
item.headings.forEach((c) => {
|
|
356
|
+
if (contains(c.safeText, term)) {
|
|
357
|
+
item.score = item.score + scoring.termHeading;
|
|
358
|
+
isTermFound = true;
|
|
359
|
+
|
|
360
|
+
if (
|
|
361
|
+
item.matchedHeadings.filter(
|
|
362
|
+
(h) => h.slug == c.slug
|
|
363
|
+
).length == 0
|
|
364
|
+
) {
|
|
365
|
+
item.matchedHeadings.push(c);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Description
|
|
371
|
+
if (contains(item.description, term)) {
|
|
372
|
+
isTermFound = true;
|
|
373
|
+
item.score = item.score + scoring.termDescription;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Tags
|
|
377
|
+
item.tags.forEach((t) => {
|
|
378
|
+
if (contains(t, term)) {
|
|
379
|
+
isTermFound = true;
|
|
380
|
+
item.score = item.score + scoring.termTags;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Keywords
|
|
385
|
+
if (contains(item.keywords, term)) {
|
|
386
|
+
isTermFound = true;
|
|
387
|
+
item.score = item.score + scoring.termKeywords;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (isTermFound) {
|
|
391
|
+
foundWords++;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
item.foundWords += foundWords;
|
|
396
|
+
|
|
397
|
+
if (item.score > 0) {
|
|
398
|
+
needles.push(item);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
needles.forEach((n) => {
|
|
403
|
+
// Bonus points for shallow results, i.e. /features over /features/something/something
|
|
404
|
+
|
|
405
|
+
if (n.depth < 5) {
|
|
406
|
+
n.score += scoring.depth;
|
|
407
|
+
n.foundWords++;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (n.depth < 4) {
|
|
411
|
+
n.score += scoring.depth;
|
|
412
|
+
n.foundWords++;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
needles.sort(function (a, b) {
|
|
417
|
+
if (b.foundWords === a.foundWords) {
|
|
418
|
+
return b.score - a.score;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return b.foundWords - a.foundWords;
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const total = needles.reduce(function (accumulator, needle) {
|
|
425
|
+
return accumulator + needle.score;
|
|
426
|
+
}, 0);
|
|
427
|
+
|
|
428
|
+
const results = siteSearchResults;
|
|
429
|
+
|
|
430
|
+
const ul = document.createElement('ul');
|
|
431
|
+
ul.className = 'site-search-results__list';
|
|
432
|
+
|
|
433
|
+
const limit = Math.min(needles.length, numberOfResults);
|
|
434
|
+
|
|
435
|
+
// @ts-ignore
|
|
436
|
+
const siteUrl = new URL(site_url);
|
|
437
|
+
|
|
438
|
+
for (let i = 0; i < limit; i++) {
|
|
439
|
+
const needle = needles[i];
|
|
440
|
+
|
|
441
|
+
const address = new URL(needle.url);
|
|
442
|
+
const isSameHost = siteUrl.host == address.host;
|
|
443
|
+
const url = isSameHost ? address.pathname : needle.url;
|
|
444
|
+
|
|
445
|
+
const listElementWrapper = document.createElement('a');
|
|
446
|
+
listElementWrapper.href = url;
|
|
447
|
+
listElementWrapper.className = 'result-wrapper';
|
|
448
|
+
|
|
449
|
+
const listElementTitle = document.createElement('span');
|
|
450
|
+
// Only highlight user query terms, not stemmed terms
|
|
451
|
+
listElementTitle.innerHTML = highlight(needle.title, queryTerms);
|
|
452
|
+
listElementTitle.className = 'result-title';
|
|
453
|
+
|
|
454
|
+
const path = document.createElement('div');
|
|
455
|
+
path.className = 'result-path';
|
|
456
|
+
|
|
457
|
+
// Split the path into segments, filter out empty segments (in case of leading slash)
|
|
458
|
+
const segments = address.pathname.split('/').filter(Boolean);
|
|
459
|
+
|
|
460
|
+
segments.forEach((segment, index) => {
|
|
461
|
+
const words = segment.replace(/-/g, ' ').split(' ');
|
|
462
|
+
const processedSegment = words
|
|
463
|
+
.map((word, index) =>
|
|
464
|
+
index === 0
|
|
465
|
+
? word.charAt(0).toUpperCase() +
|
|
466
|
+
word.slice(1).toLowerCase()
|
|
467
|
+
: word.toLowerCase()
|
|
468
|
+
)
|
|
469
|
+
.join(' ');
|
|
470
|
+
|
|
471
|
+
const segmentSpan = document.createElement('span');
|
|
472
|
+
segmentSpan.className = 'result-path-segment';
|
|
473
|
+
segmentSpan.textContent = processedSegment;
|
|
474
|
+
path.appendChild(segmentSpan);
|
|
475
|
+
|
|
476
|
+
if (index < segments.length - 1) {
|
|
477
|
+
const svgIcon = document.createElement('span');
|
|
478
|
+
svgIcon.className = 'result-path-icon';
|
|
479
|
+
svgIcon.innerHTML = `
|
|
480
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="6" height="10" viewBox="0 0 6 10">
|
|
481
|
+
<path d="M1 9L5 5L1 1" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
|
482
|
+
</svg>
|
|
483
|
+
`;
|
|
484
|
+
path.appendChild(svgIcon);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const listElementDescription = document.createElement('p');
|
|
489
|
+
listElementDescription.className = 'result-description';
|
|
490
|
+
// Only highlight user query terms, not stemmed terms
|
|
491
|
+
listElementDescription.innerHTML = highlight(
|
|
492
|
+
needle.description,
|
|
493
|
+
queryTerms
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const li = document.createElement('li');
|
|
497
|
+
li.classList.add('site-search-results-item');
|
|
498
|
+
li.dataset.words = needle.foundWords.toString();
|
|
499
|
+
li.dataset.score = (
|
|
500
|
+
Math.round((needle.score / total) * 1000) / 1000
|
|
501
|
+
).toString();
|
|
502
|
+
listElementWrapper.appendChild(path);
|
|
503
|
+
listElementWrapper.appendChild(listElementTitle);
|
|
504
|
+
listElementWrapper.appendChild(listElementDescription);
|
|
505
|
+
li.appendChild(listElementWrapper);
|
|
506
|
+
|
|
507
|
+
if (
|
|
508
|
+
enabled(f.search, 'headings') &&
|
|
509
|
+
needle.matchedHeadings.length > 0
|
|
510
|
+
) {
|
|
511
|
+
const headings = document.createElement('ul');
|
|
512
|
+
headings.className = 'result-headings';
|
|
513
|
+
|
|
514
|
+
headings.tabIndex = 0;
|
|
515
|
+
|
|
516
|
+
needle.matchedHeadings.forEach((h) => {
|
|
517
|
+
const item = document.createElement('li');
|
|
518
|
+
const link = document.createElement('a');
|
|
519
|
+
link.href = url + '#' + h.slug;
|
|
520
|
+
// Only highlight user query terms, not stemmed terms
|
|
521
|
+
link.innerHTML = highlight(h.text, queryTerms);
|
|
522
|
+
item.appendChild(link);
|
|
523
|
+
headings.append(item);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
li.appendChild(headings);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
ul.appendChild(li);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let h4;
|
|
533
|
+
if (needles.length === 0) {
|
|
534
|
+
h4 = document.createElement('h4');
|
|
535
|
+
h4.classList.add('search-results-heading');
|
|
536
|
+
h4.innerHTML = results.dataset.emptytitle || 'No Results';
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const more = document.createElement('button');
|
|
540
|
+
more.className = 'show-more';
|
|
541
|
+
more.type = 'button';
|
|
542
|
+
more.innerHTML = 'See more';
|
|
543
|
+
more.addEventListener('click', async function (e) {
|
|
544
|
+
e.stopPropagation(); // Prevent the click from closing the dropdown
|
|
545
|
+
currentQuery = '';
|
|
546
|
+
const oldTotal = numberOfResults;
|
|
547
|
+
const newTotal = numberOfResults + 12;
|
|
548
|
+
await search(s, newTotal);
|
|
549
|
+
window.setTimeout(function () {
|
|
550
|
+
const previousPosition = qs(
|
|
551
|
+
`.site-search-results__list li:nth-child(${oldTotal}) > a`
|
|
552
|
+
);
|
|
553
|
+
console.log(previousPosition.outerHTML);
|
|
554
|
+
previousPosition.focus();
|
|
555
|
+
}, 10);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
if (needles.length > numberOfResults) {
|
|
559
|
+
const moreLi = document.createElement('li');
|
|
560
|
+
moreLi.appendChild(more);
|
|
561
|
+
ul.appendChild(moreLi);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
results.innerHTML = '';
|
|
565
|
+
results.appendChild(ul);
|
|
566
|
+
h4 && results.appendChild(h4);
|
|
567
|
+
|
|
568
|
+
const address = window.location.href.split('?')[0];
|
|
569
|
+
window.history.pushState(
|
|
570
|
+
{},
|
|
571
|
+
'',
|
|
572
|
+
address + '?q=' + encodeURIComponent(cleanQuery)
|
|
573
|
+
);
|
|
574
|
+
raiseEvent('searched', { search: s });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** @type {Number} */
|
|
578
|
+
var debounceTimer;
|
|
579
|
+
|
|
580
|
+
function debounceSearch() {
|
|
581
|
+
var input = siteSearchInput;
|
|
582
|
+
|
|
583
|
+
if (input == null) {
|
|
584
|
+
throw new Error('Cannot find data-site-search-query');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Words chained with . are combined, i.e. System.Text is "systemtext"
|
|
588
|
+
var s = input.value.replace(/\./g, '').trim();
|
|
589
|
+
|
|
590
|
+
if (!s) {
|
|
591
|
+
const address = window.location.href.split('?')[0];
|
|
592
|
+
window.history.pushState({}, '', address);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
window.clearTimeout(debounceTimer);
|
|
596
|
+
debounceTimer = window.setTimeout(function () {
|
|
597
|
+
if (ready && s) {
|
|
598
|
+
search(s);
|
|
599
|
+
}
|
|
600
|
+
}, 400);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
fetch(dataUrl)
|
|
604
|
+
.then(function (response) {
|
|
605
|
+
return response.json();
|
|
606
|
+
})
|
|
607
|
+
.then(function (data) {
|
|
608
|
+
haystack = data;
|
|
609
|
+
ready = true;
|
|
610
|
+
|
|
611
|
+
for (let i = 0; i < haystack.length; i++) {
|
|
612
|
+
const item = haystack[i];
|
|
613
|
+
item.safeTitle = sanitise(item.title);
|
|
614
|
+
item.tags = item.tags.map((t) => sanitise(t));
|
|
615
|
+
item.safeDescription = sanitise(item.description);
|
|
616
|
+
item.depth = item.url.match(/\//g)?.length ?? 0;
|
|
617
|
+
|
|
618
|
+
item.headings.forEach((h) => (h.safeText = sanitise(h.text)));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** @type {HTMLFormElement} */
|
|
622
|
+
const siteSearch = siteSearchElement;
|
|
623
|
+
|
|
624
|
+
/** @type {HTMLInputElement} */
|
|
625
|
+
const siteSearchQuery = siteSearchInput;
|
|
626
|
+
|
|
627
|
+
if (siteSearch == null || siteSearchQuery == null) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
'Cannot find #site-search or data-site-search-query'
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
siteSearch.addEventListener('submit', function (e) {
|
|
634
|
+
e.preventDefault();
|
|
635
|
+
debounceSearch();
|
|
636
|
+
return false;
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
siteSearchQuery.addEventListener('keyup', function (e) {
|
|
640
|
+
e.preventDefault();
|
|
641
|
+
if (!scrolled) {
|
|
642
|
+
scrolled = true;
|
|
643
|
+
this.scrollIntoView(true);
|
|
644
|
+
}
|
|
645
|
+
debounceSearch();
|
|
646
|
+
return false;
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
const params = new URLSearchParams(window.location.search);
|
|
650
|
+
if (params.has('q')) {
|
|
651
|
+
siteSearchQuery.value = params.get('q') ?? '';
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
for (let key of Object.keys(scoring)) {
|
|
655
|
+
if (params.has(`s_${key}`)) {
|
|
656
|
+
scoring[key] = parseInt(
|
|
657
|
+
params.get(`s_${key}`) ?? scoring[key].toString(),
|
|
658
|
+
10
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
debounceSearch();
|
|
664
|
+
})
|
|
665
|
+
.catch((error) => {
|
|
666
|
+
console.log(error);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (document.readyState === 'loading') {
|
|
671
|
+
// Loading hasn't finished yet
|
|
672
|
+
document.addEventListener('DOMContentLoaded', initializeSearch);
|
|
673
|
+
} else {
|
|
674
|
+
// `DOMContentLoaded` has already fired
|
|
675
|
+
initializeSearch();
|
|
676
|
+
}
|