astro-accelerator 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +6 -0
- package/package.json +52 -0
- package/public/js/main.js +45 -0
- package/public/js/modules/animation.js +69 -0
- package/public/js/modules/click-blocks.js +41 -0
- package/public/js/modules/code-blocks.js +59 -0
- package/public/js/modules/events.js +19 -0
- package/public/js/modules/external-links.js +20 -0
- package/public/js/modules/figures.js +37 -0
- package/public/js/modules/focus.js +76 -0
- package/public/js/modules/nav-mobile.js +122 -0
- package/public/js/modules/nav-sticky.js +54 -0
- package/public/js/modules/query.js +41 -0
- package/public/js/modules/resizing.js +43 -0
- package/public/js/modules/string.js +66 -0
- package/public/js/modules/youtube.js +39 -0
- package/public/js/search.js +249 -0
- package/src/data/footer.ts +43 -0
- package/src/data/image-size.mjs +5 -0
- package/src/data/images.mjs +4 -0
- package/src/data/navigation.ts +64 -0
- package/src/layouts/Author.astro +15 -0
- package/src/layouts/Default.astro +15 -0
- package/src/layouts/Redirect.astro +15 -0
- package/src/layouts/Search.astro +15 -0
- package/src/pages/articles/feed.xml.ts +52 -0
- package/src/pages/report/missing-banner.astro +42 -0
- package/src/pages/report/missing-meta.astro +42 -0
- package/src/pages/report/oldest-content.astro +41 -0
- package/src/pages/report/taxonomy.astro +47 -0
- package/src/pages/search.json.ts +43 -0
- package/src/pages/sitemap.xml.ts +36 -0
- package/src/themes/accelerator/components/ArticleList.astro +67 -0
- package/src/themes/accelerator/components/Authors.astro +57 -0
- package/src/themes/accelerator/components/AuthorsMini.astro +37 -0
- package/src/themes/accelerator/components/Breadcrumbs.astro +33 -0
- package/src/themes/accelerator/components/Footer.astro +37 -0
- package/src/themes/accelerator/components/FooterItem.astro +29 -0
- package/src/themes/accelerator/components/Header.astro +46 -0
- package/src/themes/accelerator/components/HtmlHead.astro +49 -0
- package/src/themes/accelerator/components/Navigation.astro +31 -0
- package/src/themes/accelerator/components/NavigationBar.astro +31 -0
- package/src/themes/accelerator/components/NavigationItem.astro +37 -0
- package/src/themes/accelerator/components/Paging.astro +33 -0
- package/src/themes/accelerator/components/Related.astro +88 -0
- package/src/themes/accelerator/components/SkipLinks.astro +25 -0
- package/src/themes/accelerator/components/TableOfContents.astro +32 -0
- package/src/themes/accelerator/components/Taxonomy.astro +49 -0
- package/src/themes/accelerator/layouts/Author.astro +30 -0
- package/src/themes/accelerator/layouts/Default.astro +62 -0
- package/src/themes/accelerator/layouts/Redirect.astro +19 -0
- package/src/themes/accelerator/layouts/Search.astro +43 -0
- package/src/themes/accelerator/utilities/Authors.astro +48 -0
- package/src/themes/accelerator/utilities/Breadcrumbs.astro +27 -0
- package/src/themes/accelerator/utilities/Cache.astro +42 -0
- package/src/themes/accelerator/utilities/DateFormat.astro +25 -0
- package/src/themes/accelerator/utilities/Footer.astro +176 -0
- package/src/themes/accelerator/utilities/Languages.astro +14 -0
- package/src/themes/accelerator/utilities/Markdown.astro +27 -0
- package/src/themes/accelerator/utilities/NavPage.astro +65 -0
- package/src/themes/accelerator/utilities/Navigation.astro +81 -0
- package/src/themes/accelerator/utilities/NavigationTypes.astro +20 -0
- package/src/themes/accelerator/utilities/PageLinks.astro +72 -0
- package/src/themes/accelerator/utilities/PageQueries.astro +71 -0
- package/src/themes/accelerator/utilities/PageTypeFilters.astro +69 -0
- package/src/themes/accelerator/utilities/Taxonomy.astro +112 -0
- package/src/themes/accelerator/utilities/Url.astro +19 -0
- package/src/themes/accelerator/utilities/custom-markdown.mjs +104 -0
- package/src/themes/accelerator/utilities/img.mjs +152 -0
- package/src/themes/accelerator/utilities/language.json +111 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { qs, qsa } from './query.js';
|
|
4
|
+
import { getFocusableElement, trapFocusForward, trapReverseFocus } from './focus.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Provides an overlay with the navigation for mobile users.
|
|
8
|
+
*
|
|
9
|
+
* Example: You have site navigation on the page, but demote it (closer to the footer) on mobile to avoid
|
|
10
|
+
* the content being pushed below the fold. You provide an icon that bookmarks to the
|
|
11
|
+
* navigation.
|
|
12
|
+
*
|
|
13
|
+
* The mobile navigation intercepts the bookmark link and opens the navigation in a modal
|
|
14
|
+
* overlay, trapping keyboard focus until the overlay is closed.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} iconSelector
|
|
17
|
+
* @param {string} navigationSelector
|
|
18
|
+
* @param {string} resizedEventName
|
|
19
|
+
*/
|
|
20
|
+
function addMobileNavigation(iconSelector, navigationSelector, resizedEventName) {
|
|
21
|
+
const icon = qs(iconSelector);
|
|
22
|
+
const originalIcon = icon.innerHTML;
|
|
23
|
+
const overlay = document.createElement('div');
|
|
24
|
+
const dataOpen = 'data-open';
|
|
25
|
+
|
|
26
|
+
// Focus trap (forwards the tab / shift-tab back to the menu)
|
|
27
|
+
icon.addEventListener('keydown', function(e) {
|
|
28
|
+
if (icon.getAttribute(dataOpen) === dataOpen) {
|
|
29
|
+
var focusElements = getFocusableElement(overlay);
|
|
30
|
+
trapFocusForward(e, focusElements.first);
|
|
31
|
+
trapReverseFocus(e, focusElements.last);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Close menu on escape-key press
|
|
36
|
+
document.addEventListener('keydown', function(e) {
|
|
37
|
+
if (icon.getAttribute(dataOpen) === dataOpen) {
|
|
38
|
+
if (e.key === 'Escape') {
|
|
39
|
+
closeMobileMenu();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Opens and closes menu
|
|
45
|
+
function handleIconInteraction() {
|
|
46
|
+
if (icon.dataset.open == dataOpen) {
|
|
47
|
+
closeMobileMenu();
|
|
48
|
+
} else {
|
|
49
|
+
openMobileMenu();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function openMobileMenu(){
|
|
54
|
+
document.body.style.overflow = 'hidden';
|
|
55
|
+
const menuElement = qs(navigationSelector);
|
|
56
|
+
|
|
57
|
+
overlay.innerHTML = menuElement.outerHTML;
|
|
58
|
+
overlay.className = 'overlay overlay-menu';
|
|
59
|
+
overlay.style.display = 'block';
|
|
60
|
+
|
|
61
|
+
qsa('[id]', overlay).forEach((elem) => {
|
|
62
|
+
elem.id = 'overlay__' + elem.id
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Modal Accessibility
|
|
66
|
+
const title = qs('.site-nav-title', overlay);
|
|
67
|
+
title.setAttribute('id', 'modal-title');
|
|
68
|
+
title.setAttribute('tabindex', '-1');
|
|
69
|
+
overlay.setAttribute('role', 'dialog');
|
|
70
|
+
overlay.setAttribute('aria-modal', 'true');
|
|
71
|
+
overlay.setAttribute('aria-labelledby', 'modal-title');
|
|
72
|
+
|
|
73
|
+
// Trap Focus to Visible Overlay
|
|
74
|
+
const focusElements = getFocusableElement(overlay);
|
|
75
|
+
|
|
76
|
+
focusElements.first.addEventListener('keydown', function(e) {
|
|
77
|
+
trapReverseFocus(e, icon);
|
|
78
|
+
})
|
|
79
|
+
focusElements.last.addEventListener('keydown', function(e) {
|
|
80
|
+
trapFocusForward(e, icon);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
icon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg"
|
|
84
|
+
width="40" height="40" viewBox="0 0 24 24" stroke-width="1.5"
|
|
85
|
+
fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
86
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
87
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
88
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
89
|
+
</svg>`;
|
|
90
|
+
|
|
91
|
+
document.body.appendChild(overlay);
|
|
92
|
+
icon.setAttribute(dataOpen, dataOpen);
|
|
93
|
+
focusElements.first.focus();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function closeMobileMenu() {
|
|
97
|
+
document.body.style.overflow = 'auto';
|
|
98
|
+
|
|
99
|
+
if (icon.getAttribute(dataOpen) === dataOpen) {
|
|
100
|
+
overlay.innerHTML = '';
|
|
101
|
+
overlay.style.display = 'none';
|
|
102
|
+
document.body.removeChild(overlay);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
icon.innerHTML = originalIcon;
|
|
106
|
+
icon.removeAttribute(dataOpen);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
icon.addEventListener('click', function (e) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
handleIconInteraction();
|
|
112
|
+
return false;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
document.addEventListener(resizedEventName, function (/** @type {any} */e) {
|
|
116
|
+
if (e.detail.change.width > 0) {
|
|
117
|
+
closeMobileMenu();
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { addMobileNavigation };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { qs } from './query.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Makes an existing navigation element sticky
|
|
7
|
+
*
|
|
8
|
+
* Example: If the existing navigation is not as tall as the content, the
|
|
9
|
+
* navigation will stick to the top, allowing the user to see it as
|
|
10
|
+
* they scroll through the article
|
|
11
|
+
*
|
|
12
|
+
* @param {string} headerSelector
|
|
13
|
+
* @param {string} navigationSelector
|
|
14
|
+
* @param {string} navigationListSelector
|
|
15
|
+
*/
|
|
16
|
+
function addStickyNavigation(headerSelector, navigationSelector, navigationListSelector, resizedEventName) {
|
|
17
|
+
function setNavigationMode() {
|
|
18
|
+
const header = qs(headerSelector);
|
|
19
|
+
const navigation = qs(navigationSelector);
|
|
20
|
+
const navigationList = qs(navigationListSelector);
|
|
21
|
+
|
|
22
|
+
const buffer = 50;
|
|
23
|
+
const className = 'sticky';
|
|
24
|
+
|
|
25
|
+
const dimensions = {
|
|
26
|
+
browserHeight: window.innerHeight,
|
|
27
|
+
browserWidth: window.innerWidth,
|
|
28
|
+
headerHeight: header.clientHeight,
|
|
29
|
+
navigationHeight: navigationList.clientHeight
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Only enable sticky mode if the menu will fit vertically
|
|
33
|
+
// && where the browser is more than 860px wide
|
|
34
|
+
if (dimensions.navigationHeight < ((dimensions.browserHeight - dimensions.headerHeight) - buffer)
|
|
35
|
+
&& dimensions.browserWidth > 860) {
|
|
36
|
+
console.log('Navigation: Sticky Mode');
|
|
37
|
+
navigation.classList.add(className)
|
|
38
|
+
navigation.style.top = dimensions.headerHeight.toString() + 'px';
|
|
39
|
+
} else {
|
|
40
|
+
console.log('Navigation: Fixed Mode');
|
|
41
|
+
navigation.classList.remove(className);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setNavigationMode();
|
|
46
|
+
|
|
47
|
+
document.addEventListener(resizedEventName, function(e) {
|
|
48
|
+
if (e.detail && e.detail.change && e.detail.change.height != 0) {
|
|
49
|
+
setNavigationMode();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { addStickyNavigation };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param {string} query
|
|
6
|
+
* @param {HTMLElement} [container]
|
|
7
|
+
* @returns {HTMLElement}
|
|
8
|
+
*/
|
|
9
|
+
function qs(query, container) {
|
|
10
|
+
const target = (container)
|
|
11
|
+
? container
|
|
12
|
+
: document;
|
|
13
|
+
|
|
14
|
+
/** @type {any} */
|
|
15
|
+
const result = target.querySelector(query);
|
|
16
|
+
|
|
17
|
+
if (result) {
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
throw new Error(`No element ${query}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Utility for query selector all
|
|
26
|
+
*
|
|
27
|
+
* @param {string} query
|
|
28
|
+
* @param {HTMLElement | null} [container]
|
|
29
|
+
* @returns {NodeListOf<any>}
|
|
30
|
+
*/
|
|
31
|
+
function qsa(query, container) {
|
|
32
|
+
const target = (container)
|
|
33
|
+
? container
|
|
34
|
+
: document;
|
|
35
|
+
|
|
36
|
+
/** @type {NodeListOf<HTMLElement>} */
|
|
37
|
+
const result = target.querySelectorAll(query);
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { qs, qsa };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { raiseEvent } from './events.js';
|
|
4
|
+
|
|
5
|
+
var resizeEventName = 'resize';
|
|
6
|
+
var resizedEventName = 'resized';
|
|
7
|
+
|
|
8
|
+
var width = window.innerWidth;
|
|
9
|
+
var height = window.innerHeight;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Adds a de-bounced "resized" event, so you can listen to:
|
|
13
|
+
* document.addEventListener('resized', <handler>);
|
|
14
|
+
*
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
function addResizedEvent() {
|
|
18
|
+
let debounce = null;
|
|
19
|
+
|
|
20
|
+
function resizeEnd(e) {
|
|
21
|
+
window.clearTimeout(debounce);
|
|
22
|
+
debounce = window.setTimeout(raiseResizeEvent, 500);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function raiseResizeEvent() {
|
|
26
|
+
const change = {
|
|
27
|
+
width: window.innerWidth - width,
|
|
28
|
+
height: window.innerHeight - height
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
width = window.innerWidth;
|
|
32
|
+
height = window.innerHeight;
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
raiseEvent(resizedEventName, { change: change });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
window.addEventListener(resizeEventName, resizeEnd);
|
|
39
|
+
|
|
40
|
+
return resizedEventName;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { addResizedEvent };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Looks for a search within a string
|
|
5
|
+
*
|
|
6
|
+
* @param {string} string
|
|
7
|
+
* @param {string} search
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
function contains(string, search) {
|
|
11
|
+
return string.indexOf(search) > -1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
* @param {string} string
|
|
17
|
+
* @param {string[]} terms
|
|
18
|
+
* @returns
|
|
19
|
+
*/
|
|
20
|
+
function highlight(string, terms) {
|
|
21
|
+
terms.forEach(term => {
|
|
22
|
+
const regEx = new RegExp(term, "ig");
|
|
23
|
+
const matches = string.match(regEx);
|
|
24
|
+
if (matches) {
|
|
25
|
+
string = string.replace(regEx, `<mark>${matches[0]}</mark>`)
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Simplifies a string to plain lower case, removing diacritic characters and hyphens
|
|
33
|
+
* This means a search for "co-op" will be found in "COOP" and "Café" will be found in "cafe"
|
|
34
|
+
* @param {string} string
|
|
35
|
+
* @returns
|
|
36
|
+
*/
|
|
37
|
+
function sanitise(string) {
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
if (String.prototype.normalize) {
|
|
40
|
+
// Reduces diacritic characters to plain characters
|
|
41
|
+
string.trim().normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/-/g, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Some browsers can't normalise strings
|
|
45
|
+
return string.trim().toLowerCase().replace(/-/g, '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sets a minimum length for a search
|
|
50
|
+
* @param {string} string
|
|
51
|
+
* @returns
|
|
52
|
+
*/
|
|
53
|
+
function isLongEnough(string) {
|
|
54
|
+
return string.length > 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
*
|
|
59
|
+
* @param {string} string
|
|
60
|
+
* @returns {string[]}
|
|
61
|
+
*/
|
|
62
|
+
function explode(string) {
|
|
63
|
+
return string.split(' ').filter(isLongEnough).map(sanitise);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { contains, sanitise, explode, highlight };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { qsa } from './query.js';
|
|
4
|
+
|
|
5
|
+
function enhanceYoutubeLinks() {
|
|
6
|
+
const videos = qsa('a[href^="https://www.youtube.com/watch?v="]');
|
|
7
|
+
|
|
8
|
+
for (var video of videos) {
|
|
9
|
+
const id = new URL(video.href).searchParams.get('v');
|
|
10
|
+
video.setAttribute('data-youtube', id);
|
|
11
|
+
video.classList.add('init');
|
|
12
|
+
video.setAttribute('role', 'button');
|
|
13
|
+
|
|
14
|
+
video.innerHTML = `<div class="yt-video">
|
|
15
|
+
<div class="play-icon" style="background-image: url(https://img.youtube.com/vi/${id}/0.jpg)">▶</div>
|
|
16
|
+
<div class="title">${video.textContent}</div>
|
|
17
|
+
</div>`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clickHandler (event) {
|
|
21
|
+
var link = event.target.closest('[data-youtube]');
|
|
22
|
+
|
|
23
|
+
if (!link) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
var id = link.getAttribute('data-youtube');
|
|
29
|
+
|
|
30
|
+
var player = document.createElement('div');
|
|
31
|
+
player.innerHTML = `<iframe class="yt-iframe" width="560" height="315" src="https://www.youtube-nocookie.com/embed/${id}?autoplay=1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
|
|
32
|
+
|
|
33
|
+
link.replaceWith(player);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
document.addEventListener('click', clickHandler);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { enhanceYoutubeLinks }
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { qs } from './modules/query.js';
|
|
4
|
+
import { raiseEvent } from './modules/events.js';
|
|
5
|
+
import { contains, sanitise, explode, highlight } from './modules/string.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
type Heading = {
|
|
9
|
+
text: string;
|
|
10
|
+
safeText: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type SearchEntry = {
|
|
15
|
+
score: number;
|
|
16
|
+
title: string;
|
|
17
|
+
safeTitle: string;
|
|
18
|
+
description: string;
|
|
19
|
+
safeDescription: string;
|
|
20
|
+
headings: Heading[];
|
|
21
|
+
tags: string;
|
|
22
|
+
url: string;
|
|
23
|
+
date: string;
|
|
24
|
+
matchedHeadings: Heading[];
|
|
25
|
+
}
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
var dataUrl = qs('#site-search').dataset.sourcedata;
|
|
29
|
+
var haystack = /** @type {SearchEntry} */ [];
|
|
30
|
+
var currentQuery = '';
|
|
31
|
+
|
|
32
|
+
var ready = false;
|
|
33
|
+
var scrolled = false;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
*
|
|
37
|
+
* @param {string} s
|
|
38
|
+
* @returns
|
|
39
|
+
*/
|
|
40
|
+
function search(s) {
|
|
41
|
+
const needles = /** @type {SearchEntry} */ [];
|
|
42
|
+
|
|
43
|
+
// Clean the input
|
|
44
|
+
const cleanQuery = sanitise(s);
|
|
45
|
+
|
|
46
|
+
if (currentQuery === cleanQuery) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
raiseEvent('searched', { search: s });
|
|
51
|
+
|
|
52
|
+
currentQuery = cleanQuery;
|
|
53
|
+
const queryTerms = explode(currentQuery);
|
|
54
|
+
|
|
55
|
+
s.length > 0 && haystack.forEach( (item) => {
|
|
56
|
+
|
|
57
|
+
item.score = 0;
|
|
58
|
+
item.matchedHeadings = [];
|
|
59
|
+
|
|
60
|
+
// Imagine the user searched for "Kitchen Sink"
|
|
61
|
+
// The scores are arranged below from highest to lowest relevance
|
|
62
|
+
|
|
63
|
+
// If the title contains "Kitchen Sink"
|
|
64
|
+
if (contains(item.safeTitle, currentQuery)) {
|
|
65
|
+
item.score = item.score + 60;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If a heading contains "Kitchen Sink"
|
|
69
|
+
item.headings.forEach(c => {
|
|
70
|
+
if (contains(c.safeText, currentQuery)) {
|
|
71
|
+
item.score = item.score + 25;
|
|
72
|
+
item.matchedHeadings.push(c);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// If the title contains "Kitchen Sink"
|
|
77
|
+
if (contains(item.description, currentQuery)) {
|
|
78
|
+
item.score = item.score + 20;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
queryTerms.forEach(term => {
|
|
82
|
+
// If the title contains "Kitchen" or "Sink"
|
|
83
|
+
if (contains(item.safeTitle, term)) {
|
|
84
|
+
item.score = item.score + 40;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If a heading contains "Kitchen" or "Sink"
|
|
88
|
+
item.headings.forEach(c => {
|
|
89
|
+
if (contains(c.safeText, term)) {
|
|
90
|
+
item.score = item.score + 15;
|
|
91
|
+
if (item.matchedHeadings.filter(h => h.slug == c.slug).length == 0) {
|
|
92
|
+
item.matchedHeadings.push(c);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// If the description contains "Kitchen" or "Sink"
|
|
98
|
+
if (contains(item.description, term)) {
|
|
99
|
+
item.score = item.score + 10;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If a tag contains "Kitchen" or "Sink"
|
|
103
|
+
item.tags.forEach(t => {
|
|
104
|
+
if (contains(t, term)) {
|
|
105
|
+
item.score = item.score + 5;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (item.score > 0) {
|
|
111
|
+
needles.push(item);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
needles.sort(function (a, b){
|
|
116
|
+
return b.score - a.score;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const total = needles.reduce(function (accumulator, needle) {
|
|
120
|
+
return accumulator + needle.score;
|
|
121
|
+
}, 0);
|
|
122
|
+
|
|
123
|
+
const results = qs('#site-search-results');
|
|
124
|
+
|
|
125
|
+
const ol = document.createElement('ol');
|
|
126
|
+
ol.className = 'site-search-results';
|
|
127
|
+
|
|
128
|
+
const limit = Math.min(needles.length, 12);
|
|
129
|
+
|
|
130
|
+
// @ts-ignore
|
|
131
|
+
const siteUrl = new URL(site_url);
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < limit; i++) {
|
|
134
|
+
const needle = needles[i];
|
|
135
|
+
|
|
136
|
+
const address = new URL(needle.url);
|
|
137
|
+
const isSameHost = siteUrl.host == address.host;
|
|
138
|
+
const url = isSameHost ? address.pathname : needle.url;
|
|
139
|
+
|
|
140
|
+
const a = document.createElement('a');
|
|
141
|
+
a.innerHTML = highlight(needle.title, queryTerms);
|
|
142
|
+
a.href = url;
|
|
143
|
+
|
|
144
|
+
const path = document.createElement('div');
|
|
145
|
+
path.className = 'result-path';
|
|
146
|
+
path.innerHTML = address.pathname;
|
|
147
|
+
|
|
148
|
+
const markers = document.createElement('div');
|
|
149
|
+
markers.className = 'result-text';
|
|
150
|
+
markers.innerHTML = highlight(needle.description, queryTerms);
|
|
151
|
+
|
|
152
|
+
const headings = document.createElement('ul');
|
|
153
|
+
markers.className = 'result-headings';
|
|
154
|
+
console.log(needle.matchedHeadings);
|
|
155
|
+
needle.matchedHeadings
|
|
156
|
+
.forEach(h => {
|
|
157
|
+
const item = document.createElement('li');
|
|
158
|
+
const link = document.createElement('a');
|
|
159
|
+
link.href = url + '#' + h.slug;
|
|
160
|
+
link.innerHTML = highlight(h.text, queryTerms);
|
|
161
|
+
item.appendChild(link);
|
|
162
|
+
headings.append(item);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const li = document.createElement('li');
|
|
166
|
+
li.appendChild(a);
|
|
167
|
+
li.appendChild(path);
|
|
168
|
+
li.appendChild(markers);
|
|
169
|
+
li.append(headings);
|
|
170
|
+
li.dataset.score = (Math.round((needle.score/ total) * 100)).toString();
|
|
171
|
+
|
|
172
|
+
ol.appendChild(li);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
var h2 = document.createElement('h2');
|
|
176
|
+
h2.innerHTML = needles.length === 0
|
|
177
|
+
? results.dataset.emptytitle || 'No Results'
|
|
178
|
+
: results.dataset.title || 'Results';
|
|
179
|
+
|
|
180
|
+
results.innerHTML = '';
|
|
181
|
+
results.appendChild(h2);
|
|
182
|
+
results.appendChild(ol);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
var debounceTimer;
|
|
186
|
+
|
|
187
|
+
function debounceSearch() {
|
|
188
|
+
var input = /** @type {HTMLInputElement} */(qs('#site-search-query'));
|
|
189
|
+
|
|
190
|
+
if (input == null) {
|
|
191
|
+
throw new Error('Cannot find #site-search-query');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
var s = input.value;
|
|
195
|
+
|
|
196
|
+
window.clearTimeout(debounceTimer);
|
|
197
|
+
debounceTimer = window.setTimeout(function () {
|
|
198
|
+
if (ready) {
|
|
199
|
+
search(s);
|
|
200
|
+
}
|
|
201
|
+
}, 400);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fetch(dataUrl)
|
|
205
|
+
.then(function (response) {
|
|
206
|
+
return response.json();
|
|
207
|
+
})
|
|
208
|
+
.then(function (data) {
|
|
209
|
+
haystack = data;
|
|
210
|
+
ready = true;
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < haystack.length; i++) {
|
|
213
|
+
const item = haystack[i];
|
|
214
|
+
item.safeTitle = sanitise(item.title);
|
|
215
|
+
item.tags = item.tags.map(t => sanitise(t));
|
|
216
|
+
item.safeDescription = sanitise(item.description);
|
|
217
|
+
|
|
218
|
+
item.headings.forEach(h => h.safeText = sanitise(h.text));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
var siteSearch = qs('#site-search');
|
|
222
|
+
var siteSearchQuery = qs('#site-search-query');
|
|
223
|
+
|
|
224
|
+
if (siteSearch == null || siteSearchQuery == null) {
|
|
225
|
+
throw new Error('Cannot find #site-search or #site-search-query');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
siteSearch.addEventListener('submit', function (e) {
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
debounceSearch();
|
|
231
|
+
return false;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
siteSearchQuery.addEventListener('keyup', function (e) {
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
if (!scrolled) {
|
|
237
|
+
scrolled = true;
|
|
238
|
+
this.scrollIntoView(true);
|
|
239
|
+
}
|
|
240
|
+
debounceSearch();
|
|
241
|
+
return false;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
console.log('Search ready');
|
|
245
|
+
debounceSearch();
|
|
246
|
+
})
|
|
247
|
+
.catch((error) => {
|
|
248
|
+
console.log(error)
|
|
249
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { NavPage } from '@util/NavigationTypes.astro';
|
|
2
|
+
|
|
3
|
+
export const menu: (NavPage | 'categories' | 'tags' | 'toptags')[] = [
|
|
4
|
+
'categories',
|
|
5
|
+
'tags',
|
|
6
|
+
{
|
|
7
|
+
title: 'Quick Links',
|
|
8
|
+
url: '',
|
|
9
|
+
ariaCurrent: false,
|
|
10
|
+
isOpen: false,
|
|
11
|
+
order: 1,
|
|
12
|
+
children: [{
|
|
13
|
+
title: 'Steve Fenton',
|
|
14
|
+
url: 'https://www.stevefenton.co.uk/',
|
|
15
|
+
ariaCurrent: false,
|
|
16
|
+
isOpen: false,
|
|
17
|
+
order: 1,
|
|
18
|
+
children: []
|
|
19
|
+
},{
|
|
20
|
+
title: 'Astro',
|
|
21
|
+
url: 'https://astro.build',
|
|
22
|
+
ariaCurrent: false,
|
|
23
|
+
isOpen: false,
|
|
24
|
+
order: 1,
|
|
25
|
+
children: []
|
|
26
|
+
},{
|
|
27
|
+
title: 'GitHub',
|
|
28
|
+
url: 'https://github.com/Steve-Fenton/astro-accelerator',
|
|
29
|
+
ariaCurrent: false,
|
|
30
|
+
isOpen: false,
|
|
31
|
+
order: 1,
|
|
32
|
+
children: []
|
|
33
|
+
}]
|
|
34
|
+
}];
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
See navigation.ts
|
|
38
|
+
Allows customisation of the footer navigation
|
|
39
|
+
|
|
40
|
+
'categories' -> Auto columns of links for categories
|
|
41
|
+
'tags' -> Auto columns of links for tags
|
|
42
|
+
|
|
43
|
+
*/
|