astro-accelerator 4.1.5 → 4.1.7
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 +5 -5
- package/public/css/main.css +9 -19
- package/public/js/search.js +22 -6
- package/public/js/search2.js +676 -0
- package/public/js/synonyms.js +14 -0
- package/src/config.ts +1 -1
- package/src/themes/accelerator/components/HtmlHead.astro +8 -0
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.1.
|
|
2
|
+
"version": "4.1.7",
|
|
3
3
|
"author": "Steve Fenton",
|
|
4
4
|
"name": "astro-accelerator",
|
|
5
5
|
"description": "A super-lightweight, accessible, SEO-friendly starter project for Astro",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@astrojs/mdx": "^2.3.1",
|
|
35
|
-
"astro": "^4.16.
|
|
36
|
-
"astro-accelerator-utils": "^0.3.
|
|
37
|
-
"cspell": "^8.15.
|
|
35
|
+
"astro": "^4.16.8",
|
|
36
|
+
"astro-accelerator-utils": "^0.3.26",
|
|
37
|
+
"cspell": "^8.15.7",
|
|
38
38
|
"csv": "^6.3.10",
|
|
39
39
|
"glob": "^11.0.0",
|
|
40
40
|
"hast-util-from-selector": "^3.0.0",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"sharp": "^0.33.5"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@playwright/test": "^1.48.
|
|
48
|
+
"@playwright/test": "^1.48.2"
|
|
49
49
|
},
|
|
50
50
|
"files": [
|
|
51
51
|
".npmrc",
|
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
|
@@ -72,6 +72,7 @@ function initializeSearch() {
|
|
|
72
72
|
var currentQuery = '';
|
|
73
73
|
var dataUrl = siteSearchElement.dataset.sourcedata;
|
|
74
74
|
|
|
75
|
+
// Legacy scoring
|
|
75
76
|
var scoring = {
|
|
76
77
|
depth: 5,
|
|
77
78
|
phraseTitle: 60,
|
|
@@ -84,6 +85,14 @@ function initializeSearch() {
|
|
|
84
85
|
termKeywords: 15,
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
// Found word scoring
|
|
89
|
+
const scores = {
|
|
90
|
+
titleExact: 20,
|
|
91
|
+
titleContains: 15,
|
|
92
|
+
headingContains: 10,
|
|
93
|
+
contentContains: 1,
|
|
94
|
+
};
|
|
95
|
+
|
|
87
96
|
var ready = false;
|
|
88
97
|
var scrolled = false;
|
|
89
98
|
|
|
@@ -246,6 +255,7 @@ function initializeSearch() {
|
|
|
246
255
|
/**
|
|
247
256
|
* Replaces synonyms
|
|
248
257
|
* @param {string[]} queryTerms
|
|
258
|
+
* @returns {Promise<string[]>}
|
|
249
259
|
*/
|
|
250
260
|
async function replaceSynonyms(queryTerms) {
|
|
251
261
|
const synonyms = await getSynonyms();
|
|
@@ -253,11 +263,17 @@ function initializeSearch() {
|
|
|
253
263
|
for (let i = 0; i < queryTerms.length; i++) {
|
|
254
264
|
const term = queryTerms[i];
|
|
255
265
|
if (synonyms[term] != null) {
|
|
256
|
-
|
|
266
|
+
if (synonyms[term].length === 0) {
|
|
267
|
+
// @ts-ignore
|
|
268
|
+
queryTerms[i] = null;
|
|
269
|
+
} else {
|
|
270
|
+
queryTerms.push(synonyms[term]);
|
|
271
|
+
}
|
|
257
272
|
}
|
|
258
273
|
}
|
|
259
274
|
|
|
260
|
-
|
|
275
|
+
console.log('Post-synonym', queryTerms);
|
|
276
|
+
return queryTerms.filter((qt) => qt != null);
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
/**
|
|
@@ -314,12 +330,12 @@ function initializeSearch() {
|
|
|
314
330
|
|
|
315
331
|
// Title
|
|
316
332
|
if (item.safeTitle === currentQuery) {
|
|
317
|
-
item.foundWords +=
|
|
333
|
+
item.foundWords += scores.titleExact;
|
|
318
334
|
}
|
|
319
335
|
|
|
320
336
|
if (contains(item.safeTitle, currentQuery)) {
|
|
321
337
|
item.score = item.score + scoring.phraseTitle;
|
|
322
|
-
item.foundWords +=
|
|
338
|
+
item.foundWords += scores.titleContains;
|
|
323
339
|
}
|
|
324
340
|
|
|
325
341
|
// Headings
|
|
@@ -327,14 +343,14 @@ function initializeSearch() {
|
|
|
327
343
|
if (contains(c.safeText, currentQuery)) {
|
|
328
344
|
item.score = item.score + scoring.phraseHeading;
|
|
329
345
|
item.matchedHeadings.push(c);
|
|
330
|
-
item.foundWords
|
|
346
|
+
item.foundWords += scores.headingContains;
|
|
331
347
|
}
|
|
332
348
|
});
|
|
333
349
|
|
|
334
350
|
// Description
|
|
335
351
|
if (contains(item.description, currentQuery)) {
|
|
336
352
|
item.score = item.score + scoring.phraseDescription;
|
|
337
|
-
item.foundWords
|
|
353
|
+
item.foundWords += scores.contentContains;
|
|
338
354
|
}
|
|
339
355
|
|
|
340
356
|
// Part 2 - Term Matches, i.e. "Kitchen" or "Sink"
|
|
@@ -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
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -26,6 +26,11 @@ const canonicalURL = accelerator.urlFormatter.formatUrl(new URL(Astro.url.pathn
|
|
|
26
26
|
const socialTitle = await accelerator.markdown.getTextFrom(frontmatter?.title);
|
|
27
27
|
const title = `${ accelerator.markdown.titleCase(socialTitle) } ${ ((frontmatter.titleAdditional) ? ` ${frontmatter.titleAdditional}` : '') } | ${ SITE.title }`;
|
|
28
28
|
|
|
29
|
+
const authorList = accelerator.authors.forPost(frontmatter);
|
|
30
|
+
const authorMeta = (authorList.mainAuthor?.frontmatter?.meta && authorList.mainAuthor?.frontmatter?.meta?.length > 0)
|
|
31
|
+
? authorList.mainAuthor.frontmatter.meta
|
|
32
|
+
: [];
|
|
33
|
+
|
|
29
34
|
stats.stop();
|
|
30
35
|
---
|
|
31
36
|
<head>
|
|
@@ -52,6 +57,9 @@ stats.stop();
|
|
|
52
57
|
<meta property="og:description" content={ frontmatter.description } />
|
|
53
58
|
<meta property="og:image" content={ canonicalImageSrc } />
|
|
54
59
|
<meta property="og:image:alt" content={ imageAlt } />
|
|
60
|
+
{authorMeta.map(m =>
|
|
61
|
+
<meta name={ m.name } content={ m.content } />
|
|
62
|
+
)}
|
|
55
63
|
<meta name="twitter:card" content="summary_large_image" />
|
|
56
64
|
<meta name="twitter:title" content={ socialTitle } />
|
|
57
65
|
<meta name="twitter:description" content={ frontmatter.description } />
|