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 CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.1.6",
2
+ "version": "4.1.8",
3
3
  "author": "Steve Fenton",
4
4
  "name": "astro-accelerator",
5
5
  "description": "A super-lightweight, accessible, SEO-friendly starter project for Astro",
@@ -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 li {
878
- display: none;
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:focus-within::after {
896
- display: none;
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 {
@@ -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
- queryTerms.push(synonyms[term]);
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 += 2;
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 += 2;
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.foundWords === a.foundWords) {
417
- return b.score - a.score;
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.foundWords - a.foundWords;
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
+ }
@@ -0,0 +1,14 @@
1
+ /** @format */
2
+
3
+ const synonyms = {
4
+ // Keep me alphabetical
5
+ // Removes join words
6
+ and: '',
7
+ for: '',
8
+ if: '',
9
+ the: '',
10
+ to: '',
11
+ with: '',
12
+ };
13
+
14
+ export { synonyms };
package/src/config.ts CHANGED
@@ -40,7 +40,7 @@ const SITE: Site = {
40
40
  youTubeLinks: ['embed'],
41
41
  headers: ['link'],
42
42
  details: ['tabs'],
43
- search: [],
43
+ search: ['headings'],
44
44
  },
45
45
  images: {
46
46
  // Generated using https://ausi.github.io/respimagelint/