Rewrite half the theme to make Mozilla observatory more happy about strict CSP

This commit is contained in:
daudix
2024-10-18 18:16:38 +03:00
parent 932c7d9aa4
commit 26b12aadf6
19 changed files with 730 additions and 679 deletions

View File

@ -20,11 +20,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- **[BREAKING]** Rename the visually hidden `hidden` class to `visually-hidden`. `hidden` is now used to completely hide the elements, including screen readers.
- Make `emoji` class available outside of comments. - Make `emoji` class available outside of comments.
- Make the code and styling for article cards much cleaner. - Make the code and styling for article cards much cleaner.
- Make the shortcodes code much cleaner. - Make the shortcodes code much cleaner.
- Remove top/bottom margin from the first/last element in `<aside>`. - Remove top/bottom margin from the first/last element in `<aside>`.
- Rename `show_read_time` to `show_reading_time`. - Rename `show_read_time` to `show_reading_time`.
- Replace "Go Back" button with "Go Home" on 404.
- Rewrite JavaScript scripts to not rely on being inline, but rather function as separate files, as required by Content Security Policy.
- Tighten the default Content Security Policy.
- Use Zola's built-in reading time variable instead of the custom implementation (#102). - Use Zola's built-in reading time variable instead of the custom implementation (#102).
### Fixed ### Fixed

View File

@ -165,10 +165,10 @@ csp = [
{ directive = "font-src", domains = ["'self'", "data:"] }, { directive = "font-src", domains = ["'self'", "data:"] },
{ directive = "img-src", domains = ["'self'", "https:", "data:"] }, { directive = "img-src", domains = ["'self'", "https:", "data:"] },
{ directive = "media-src", domains = ["'self'", "https:", "data:"] }, { directive = "media-src", domains = ["'self'", "https:", "data:"] },
{ directive = "script-src", domains = ["'self'", "'unsafe-inline'"] }, { directive = "script-src", domains = ["'self'"] },
{ directive = "style-src", domains = ["'self'", "'unsafe-inline'"] }, { directive = "style-src", domains = ["'self'", "'unsafe-inline'"] },
{ directive = "frame-src", domains = ["https://player.vimeo.com", "https://www.youtube-nocookie.com", "https://toot.community"] }, { directive = "frame-src", domains = ["https://player.vimeo.com", "https://www.youtube-nocookie.com", "https://toot.community"] },
{ directive = "connect-src", domains = ["https:"] }, { directive = "connect-src", domains = ["https://toot.community"] },
] ]
# Display outlines around all elements for debugging purposes # Display outlines around all elements for debugging purposes
# debug_layout = true # debug_layout = true

View File

@ -1,9 +1,4 @@
.hidden { .hidden {
clip: rect(0 0 0 0); display: none;
position: absolute !important; visibility: hidden;
clip-path: inset(100%);
width: 1px !important;
height: 1px !important;
overflow: hidden;
white-space: nowrap;
} }

View File

@ -0,0 +1,9 @@
.visually-hidden {
clip: rect(0 0 0 0);
position: absolute !important;
clip-path: inset(100%);
width: 1px !important;
height: 1px !important;
overflow: hidden;
white-space: nowrap;
}

View File

@ -36,4 +36,5 @@
@use "tags"; @use "tags";
@use "title"; @use "title";
@use "typography"; @use "typography";
@use "visually-hidden";
@use "zola-anchor"; @use "zola-anchor";

349
static/comments.js Normal file
View File

@ -0,0 +1,349 @@
// Taken from https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/
// Attachment code taken from https://github.com/cassidyjames/cassidyjames.github.io/blob/99782788a7e3ba3cc52d6803010873abd1b02b9e/_includes/comments.html#L251-L296
let relAttributes = document.getElementById("rel-attributes").textContent;
let host = document.getElementById("host").textContent;
let user = document.getElementById("user").textContent;
let id = document.getElementById("id").textContent;
let dateLocale = document.getElementById("date-locale").textContent;
let loadingText = document.getElementById("loading-text").textContent;
let reloadText = document.getElementById("reload-text").textContent;
let viewProfileText = document.getElementById("view-profile-text").textContent;
let viewCommentText = document.getElementById("view-comment-text").textContent;
let boostsFromText = document.getElementById("boosts-from-text").textContent;
let favesFromText = document.getElementById("faves-from-text").textContent;
let blogPostAuthorText = document.getElementById("blog-post-author-text").textContent;
let noCommentsText = document.getElementById("no-comments-text").textContent;
document.getElementById("load-comments").addEventListener("click", loadComments);
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function emojify(input, emojis) {
let output = input;
emojis.forEach((emoji) => {
let picture = document.createElement("picture");
let source = document.createElement("source");
source.setAttribute("srcset", escapeHtml(emoji.url));
source.setAttribute("media", "(prefers-reduced-motion: no-preference)");
let img = document.createElement("img");
img.className = "emoji";
img.setAttribute("src", escapeHtml(emoji.static_url));
img.setAttribute("alt", `:${emoji.shortcode}:`);
img.setAttribute("title", `:${emoji.shortcode}:`);
// {% - if config.markdown.lazy_async_image -%}
// img.setAttribute("decoding", "async");
// img.setAttribute("loading", "lazy");
// {% - endif -%}
picture.appendChild(source);
picture.appendChild(img);
output = output.replace(`:${emoji.shortcode}:`, picture.outerHTML);
});
return output;
}
function loadComments() {
let commentsWrapper = document.getElementById("comments-wrapper");
commentsWrapper.innerHTML = "";
let loadCommentsButton = document.getElementById("load-comments");
loadCommentsButton.innerHTML = loadingText;
loadCommentsButton.disabled = true;
fetch(`https://${host}/api/v1/statuses/${id}/context`)
.then(function (response) {
return response.json();
})
.then(function (data) {
let descendants = data["descendants"];
if (
descendants &&
Array.isArray(descendants) &&
descendants.length > 0
) {
commentsWrapper.innerHTML = "";
descendants.forEach(function (status) {
console.log(descendants);
if (status.account.display_name.length > 0) {
status.account.display_name = escapeHtml(
status.account.display_name
);
status.account.display_name = emojify(
status.account.display_name,
status.account.emojis
);
} else {
status.account.display_name = status.account.username;
}
let instance = "";
if (status.account.acct.includes("@")) {
instance = status.account.acct.split("@")[1];
} else {
instance = host;
}
const isReply = status.in_reply_to_id !== id;
let op = false;
if (status.account.acct == user) {
op = true;
}
status.content = emojify(status.content, status.emojis);
let avatarSource = document.createElement("source");
avatarSource.setAttribute(
"srcset",
escapeHtml(status.account.avatar)
);
avatarSource.setAttribute(
"media",
"(prefers-reduced-motion: no-preference)"
);
let avatarImg = document.createElement("img");
avatarImg.className = "avatar";
avatarImg.setAttribute(
"src",
escapeHtml(status.account.avatar_static)
);
avatarImg.setAttribute(
"alt",
`@${status.account.username}@${instance} avatar`
);
// {% - if config.markdown.lazy_async_image -%}
// avatarImg.setAttribute("decoding", "async");
// avatarImg.setAttribute("loading", "lazy");
// {% - endif -%}
let avatarPicture = document.createElement("picture");
avatarPicture.appendChild(avatarSource);
avatarPicture.appendChild(avatarImg);
let avatar = document.createElement("a");
avatar.className = "avatar-link";
avatar.setAttribute("href", status.account.url);
avatar.setAttribute("rel", relAttributes);
avatar.setAttribute(
"title",
`${viewProfileText} @${status.account.username}@${instance}`
);
avatar.appendChild(avatarPicture);
let instanceBadge = document.createElement("a");
instanceBadge.className = "instance";
instanceBadge.setAttribute("href", status.account.url);
instanceBadge.setAttribute(
"title",
`@${status.account.username}@${instance}`
);
instanceBadge.setAttribute("rel", relAttributes);
instanceBadge.textContent = instance;
let display = document.createElement("span");
display.className = "display";
display.setAttribute("itemprop", "author");
display.setAttribute("itemtype", "http://schema.org/Person");
display.innerHTML = status.account.display_name;
let header = document.createElement("header");
header.className = "author";
header.appendChild(display);
header.appendChild(instanceBadge);
let permalink = document.createElement("a");
permalink.setAttribute("href", status.url);
permalink.setAttribute("itemprop", "url");
permalink.setAttribute("title", `${viewCommentText} ${instance}`);
permalink.setAttribute("rel", relAttributes);
permalink.textContent = new Date(
status.created_at
).toLocaleString(dateLocale, {
dateStyle: "long",
timeStyle: "short",
});
let timestamp = document.createElement("time");
timestamp.setAttribute("datetime", status.created_at);
timestamp.appendChild(permalink);
permalink.classList.add("external");
let main = document.createElement("main");
main.setAttribute("itemprop", "text");
main.innerHTML = status.content;
let attachments = status.media_attachments;
let SUPPORTED_MEDIA = ["image", "video", "gifv", "audio"];
let media = document.createElement("div");
media.className = "attachments";
if (
attachments &&
Array.isArray(attachments) &&
attachments.length > 0
) {
attachments.forEach((attachment) => {
if (SUPPORTED_MEDIA.includes(attachment.type)) {
let mediaElement;
switch (attachment.type) {
case "image":
mediaElement = document.createElement("img");
mediaElement.setAttribute("src", attachment.preview_url);
if (attachment.description != null) {
mediaElement.setAttribute("alt", attachment.description);
mediaElement.setAttribute("title", attachment.description);
}
// {% - if config.markdown.lazy_async_image -%}
// mediaElement.setAttribute("decoding", "async");
// mediaElement.setAttribute("loading", "lazy");
// {% - endif -%}
media.appendChild(mediaElement);
break;
case "video":
mediaElement = document.createElement("video");
mediaElement.setAttribute("src", attachment.url);
mediaElement.setAttribute("controls", "");
if (attachment.description != null) {
mediaElement.setAttribute("aria-title", attachment.description);
mediaElement.setAttribute("title", attachment.description);
}
media.appendChild(mediaElement);
break;
case "gifv":
mediaElement = document.createElement("video");
mediaElement.setAttribute("src", attachment.url);
mediaElement.setAttribute("autoplay", "");
mediaElement.setAttribute("playsinline", "");
mediaElement.setAttribute("loop", "");
if (attachment.description != null) {
mediaElement.setAttribute("aria-title", attachment.description);
mediaElement.setAttribute("title", attachment.description);
}
media.appendChild(mediaElement);
break;
case "audio":
mediaElement = document.createElement("audio");
mediaElement.setAttribute("src", attachment.url);
mediaElement.setAttribute("controls", "");
if (attachment.description != null) {
mediaElement.setAttribute("aria-title", attachment.description);
mediaElement.setAttribute("title", attachment.description);
}
media.appendChild(mediaElement);
break;
}
let mediaLink = document.createElement("a");
mediaLink.setAttribute("href", attachment.url);
mediaLink.setAttribute("rel", relAttributes);
mediaLink.appendChild(mediaElement);
media.appendChild(mediaLink);
}
});
}
let interactions = document.createElement("footer");
let boosts = document.createElement("a");
boosts.className = "boosts";
boosts.setAttribute("href", `${status.url}/reblogs`);
boosts.setAttribute("title", `${boostsFromText}`.replace("$INSTANCE", instance));
let boostsIcon = document.createElement("i");
boostsIcon.className = "icon";
boosts.appendChild(boostsIcon);
boosts.insertAdjacentHTML('beforeend', ` ${status.reblogs_count}`);
interactions.appendChild(boosts);
let faves = document.createElement("a");
faves.className = "faves";
faves.setAttribute("href", `${status.url}/favourites`);
faves.setAttribute("title", `${favesFromText}`.replace("$INSTANCE", instance));
let favesIcon = document.createElement("i");
favesIcon.className = "icon";
faves.appendChild(favesIcon);
faves.insertAdjacentHTML('beforeend', ` ${status.favourites_count}`);
interactions.appendChild(faves);
let comment = document.createElement("article");
comment.id = `comment-${status.id}`;
comment.className = isReply ? "comment comment-reply" : "comment";
comment.setAttribute("itemprop", "comment");
comment.setAttribute("itemtype", "http://schema.org/Comment");
comment.appendChild(avatar);
comment.appendChild(header);
comment.appendChild(timestamp);
comment.appendChild(main);
if (
attachments &&
Array.isArray(attachments) &&
attachments.length > 0
) {
comment.appendChild(media);
}
comment.appendChild(interactions);
if (op === true) {
comment.classList.add("op");
avatar.classList.add("op");
avatar.setAttribute(
"title",
`${blogPostAuthorText}: ` + avatar.getAttribute("title")
);
instanceBadge.classList.add("op");
instanceBadge.setAttribute(
"title",
`${blogPostAuthorText}: ` + instanceBadge.getAttribute("title")
);
}
commentsWrapper.innerHTML += comment.outerHTML;
});
}
else {
var statusText = document.createElement("p");
statusText.innerHTML = noCommentsText;
statusText.setAttribute("id", "comments-status");
commentsWrapper.appendChild(statusText);
}
loadCommentsButton.innerHTML = reloadText;
})
.catch(function (error) {
console.error('Error loading comments:', error);
})
.finally(function () {
loadCommentsButton.disabled = false;
});
}

57
static/copy-button.js Normal file
View File

@ -0,0 +1,57 @@
// Based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html
document.addEventListener("DOMContentLoaded", function () {
let blocks = document.querySelectorAll("pre[class^='language-']");
blocks.forEach((block) => {
if (navigator.clipboard) {
// Code block header title
let title = document.createElement("span");
let lang = block.getAttribute("data-lang");
title.innerHTML = lang;
// Copy button icon
let icon = document.createElement("i");
icon.classList.add("icon");
// Copy button
let button = document.createElement("button");
let copyCodeText = document.getElementById("copy-code-text").textContent;
button.setAttribute("title", copyCodeText)
button.appendChild(icon);
// Code block header
let header = document.createElement("div");
header.classList.add("header");
header.appendChild(title);
header.appendChild(button);
// Container that holds header and the code block itself
let container = document.createElement("div");
container.classList.add("pre-container");
container.appendChild(header);
// Move code block into the container
block.parentNode.insertBefore(container, block);
container.appendChild(block);
button.addEventListener("click", async () => {
await copyCode(block, header, button); // Pass the button here
});
}
});
async function copyCode(block, header, button) {
let code = block.querySelector("code");
let text = code.innerText;
await navigator.clipboard.writeText(text);
header.classList.add("active");
button.setAttribute("disabled", true);
header.addEventListener("animationend", () => {
header.classList.remove("active");
button.removeAttribute("disabled");
}, { once: true });
}
});

View File

@ -0,0 +1,209 @@
// Based on https://github.com/getzola/zola/blob/1ac1231de1e342bbaf4d7a51a8a9a40ea152e246/docs/static/search.js
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
};
}
// Taken from mdbook
// The strategy is as follows:
// First, assign a value to each word in the document:
// Words that correspond to search terms (stemmer aware): 40
// Normal words: 2
// First word in a sentence: 8
// Then use a sliding window with a constant number of words and count the
// sum of the values of the words within the window. Then use the window that got the
// maximum sum. If there are multiple maximas, then get the last one.
// Enclose the terms in <b>.
function makeTeaser(body, terms) {
var TERM_WEIGHT = 40;
var NORMAL_WORD_WEIGHT = 2;
var FIRST_WORD_WEIGHT = 8;
var TEASER_MAX_WORDS = 30;
var stemmedTerms = terms.map(function (w) {
return elasticlunr.stemmer(w.toLowerCase());
});
var termFound = false;
var index = 0;
var weighted = []; // contains elements of ["word", weight, index_in_document]
// split in sentences, then words
var sentences = body.toLowerCase().split(". ");
for (var i in sentences) {
var words = sentences[i].split(" ");
var value = FIRST_WORD_WEIGHT;
for (var j in words) {
var word = words[j];
if (word.length > 0) {
for (var k in stemmedTerms) {
if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
value = TERM_WEIGHT;
termFound = true;
}
}
weighted.push([word, value, index]);
value = NORMAL_WORD_WEIGHT;
}
index += word.length;
index += 1; // ' ' or '.' if last word in sentence
}
index += 1; // because we split at a two-char boundary '. '
}
if (weighted.length === 0) {
return body;
}
var windowWeights = [];
var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS);
// We add a window with all the weights first
var curSum = 0;
for (var i = 0; i < windowSize; i++) {
curSum += weighted[i][1];
}
windowWeights.push(curSum);
for (var i = 0; i < weighted.length - windowSize; i++) {
curSum -= weighted[i][1];
curSum += weighted[i + windowSize][1];
windowWeights.push(curSum);
}
// If we didn't find the term, just pick the first window
var maxSumIndex = 0;
if (termFound) {
var maxFound = 0;
// backwards
for (var i = windowWeights.length - 1; i >= 0; i--) {
if (windowWeights[i] > maxFound) {
maxFound = windowWeights[i];
maxSumIndex = i;
}
}
}
var teaser = [];
var startIndex = weighted[maxSumIndex][2];
for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
var word = weighted[i];
if (startIndex < word[2]) {
// missing text from index to start of `word`
teaser.push(body.substring(startIndex, word[2]));
startIndex = word[2];
}
// add <strong> around search terms
if (word[1] === TERM_WEIGHT) {
teaser.push("<strong>");
}
startIndex = word[2] + word[0].length;
teaser.push(body.substring(word[2], startIndex));
if (word[1] === TERM_WEIGHT) {
teaser.push("</strong>");
}
}
teaser.push("…");
return teaser.join("");
}
function formatSearchResultItem(item, terms) {
return '<div class="item">'
+ `<a href="${item.ref}">${item.doc.title}</a>`
+ `<span>${makeTeaser(item.doc.body, terms)}</span>`
+ '</div>';
}
function initSearch() {
var searchBar = document.getElementById("search-bar");
var searchContainer = document.getElementById("search-container");
var searchResults = document.getElementById("search-results");
var MAX_ITEMS = 10;
var options = {
bool: "AND",
fields: {
title: { boost: 2 },
body: { boost: 1 },
}
};
var currentTerm = "";
var index;
var initIndex = async function () {
if (index === undefined) {
let searchIndex = document.getElementById("search-index").textContent;
index = fetch(searchIndex)
.then(
async function (response) {
return await elasticlunr.Index.load(await response.json());
}
);
}
let res = await index;
return res;
}
searchBar.addEventListener("keyup", debounce(async function () {
var term = searchBar.value.trim();
if (term === currentTerm) {
return;
}
searchResults.style.display = term === "" ? "none" : "flex";
searchResults.innerHTML = "";
currentTerm = term;
if (term === "") {
return;
}
var results = (await initIndex()).search(term, options);
if (results.length === 0) {
searchResults.style.display = "none";
return;
}
for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
searchResults.innerHTML += formatSearchResultItem(results[i], term.split(" "));
}
}, 150));
document.addEventListener("keydown", function (event) {
if (event.key === "/") {
event.preventDefault();
toggleSearch();
}
});
document.getElementById("search-toggle").addEventListener("click", toggleSearch);
}
function toggleSearch() {
var searchContainer = document.getElementById("search-container");
var searchBar = document.getElementById("search-bar");
searchContainer.classList.toggle("active");
searchBar.toggleAttribute("disabled");
searchBar.focus();
}
if (document.readyState === "complete" ||
(document.readyState !== "loading" && !document.documentElement.doScroll)
) {
initSearch();
} else {
document.addEventListener("DOMContentLoaded", initSearch);
}

View File

@ -1,13 +1,11 @@
{#- Based on https://codeberg.org/daudix/duckquill/issues/101#issuecomment-2377169 -#} // Based on https://codeberg.org/daudix/duckquill/issues/101#issuecomment-2377169
<script type="text/javascript">
let searchSetup = false; let searchSetup = false;
let fuse; let fuse;
async function initIndex() { async function initIndex() {
if (searchSetup) return; if (searchSetup) return;
const url = "{{ get_url(path='/', lang=lang) }}/search_index.{{ config.default_language }}.json"; const url = document.getElementById("search-index").textContent;
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
@ -99,7 +97,8 @@
} }
if (match.indices.length > 4) { if (match.indices.length > 4) {
output += `<span class="more-matches">{{ macros_translate::translate(key="more_matches", default="$MATCHES more matches", language_strings=language_strings) }}</span>`.replace("$MATCHES", `+${match.indices.length - MAX_RESULTS}`); const moreMatchesText = document.getElementById("more-matches-text").textContent;
output += `<span class="more-matches">${moreMatchesText}</span>`.replace("$MATCHES", `+${match.indices.length - MAX_RESULTS}`);
} }
} }
return output + "</div>"; return output + "</div>";
@ -117,6 +116,8 @@
toggleSearch(); toggleSearch();
} }
}); });
document.getElementById("search-toggle").addEventListener("click", toggleSearch);
} }
if (document.readyState === "complete" || if (document.readyState === "complete" ||
@ -124,4 +125,3 @@
initSearch(); initSearch();
else else
document.addEventListener("DOMContentLoaded", initSearch); document.addEventListener("DOMContentLoaded", initSearch);
</script>

View File

@ -85,6 +85,16 @@ function updateActiveButton(theme) {
} }
} }
document.getElementById("theme-light").addEventListener("click", function () {
switchTheme("light");
});
document.getElementById("theme-dark").addEventListener("click", function () {
switchTheme("dark");
});
document.getElementById("theme-system").addEventListener("click", function () {
switchTheme("system");
});
// Update icon class on page load based on current theme // Update icon class on page load based on current theme
const currentTheme = localStorage.getItem("theme") || window.defaultTheme || "system"; const currentTheme = localStorage.getItem("theme") || window.defaultTheme || "system";
updateIconClass(currentTheme); updateIconClass(currentTheme);

View File

@ -12,7 +12,7 @@
<p>The requested page could not be found.{% if config.extra.issues_url %} If you feel this is not normal, then you can create an issue on the issue tracker.{% endif %}</p> <p>The requested page could not be found.{% if config.extra.issues_url %} If you feel this is not normal, then you can create an issue on the issue tracker.{% endif %}</p>
<div class="dialog-buttons"> <div class="dialog-buttons">
<button class="inline-button" onclick="window.history.go(-1)">Go Back</button> <a class="inline-button" href="{{ get_url(path='/', lang=lang) }}">Go Home</a>
{%- if config.extra.issues_url %} {%- if config.extra.issues_url %}
<a class="inline-button colored external" href="{{ config.extra.issues_url }}" rel="{{ rel_attributes }}">File an Issue</a> <a class="inline-button colored external" href="{{ config.extra.issues_url }}" rel="{{ rel_attributes }}">File an Issue</a>
{%- endif %} {%- endif %}

View File

@ -135,7 +135,62 @@
</article> </article>
{%- if page.extra.comments.id -%} {%- if page.extra.comments.id -%}
{% include "partials/comments.html" %} {%- if page.extra.comments.host -%}
{%- set host = page.extra.comments.host -%}
{%- else -%}
{%- set host = config.extra.comments.host -%}
{%- endif -%}
{%- if page.extra.comments.user -%}
{%- set user = page.extra.comments.user -%}
{%- else %}
{%- set user = config.extra.comments.user -%}
{%- endif -%}
{%- set id = page.extra.comments.id -%}
{%- set date_locale = macros_translate::translate(key="date_locale", default="en-US", language_strings=language_strings) | replace(from="_", to="-") -%}
<span id="rel-attributes" class="hidden">{{ rel_attributes }}</span>
<span id="host" class="hidden">{{ host }}</span>
<span id="user" class="hidden">{{ user }}</span>
<span id="id" class="hidden">{{ id }}</span>
<span id="date-locale" class="hidden">{{ date_locale }}</span>
<span id="loading-text" class="hidden">{{ macros_translate::translate(key='loading', default='Loading', language_strings=language_strings) }}…</span>
<span id="reload-text" class="hidden">{{ macros_translate::translate(key='reload', default='Reload', language_strings=language_strings) }}</span>
<span id="view-profile-text" class="hidden">{{ macros_translate::translate(key="view_profile", default="View Profile At", language_strings=language_strings) }}</span>
<span id="view-comment-text" class="hidden">{{ macros_translate::translate(key="view_comment", default="View Comment At", language_strings=language_strings) }}</span>
<span id="boosts-from-text" class="hidden">{{ macros_translate::translate(key="boosts_from", default="Boosts from $INSTANCE", language_strings=language_strings) }}</span>
<span id="faves-from-text" class="hidden">{{ macros_translate::translate(key="faves_from", default="Favorites from $INSTANCE", language_strings=language_strings) }}</span>
<span id="blog-post-author-text" class="hidden">{{ macros_translate::translate(key='blog_post_author', default='Blog post author', language_strings=language_strings) }}</span>
<span id="no-comments-text" class="hidden">{{ macros_translate::translate(key='no_comments', default='No Comments yet :/', language_strings=language_strings) }}</span>
<section id="comments">
{%- if config.extra.comments.show_qr -%}
<img
id="qrcode"
class="pixels no-hover"
title="{{ macros_translate::translate(key='comments_qr', default='QR code to a Mastodon post', language_strings=language_strings) }}"
{%- if config.markdown.lazy_async_image -%}decoding="async" loading="lazy"{%- endif -%}
src="https://api.qrserver.com/v1/create-qr-code/?data=https://{{ host }}/@{{ user }}/{{ id }}"
/>
{%- endif -%}
<h2>{{ macros_translate::translate(key="comments", default="Comments", language_strings=language_strings) }}</h2>
<p>{{ macros_translate::translate(key="comments_description", default="You can comment on this blog post by publicly replying to this post using a Mastodon or other ActivityPub/Fediverse account. Known non-private replies are displayed below.", language_strings=language_strings) }}</p>
<div class="dialog-buttons">
<button id="load-comments" class="inline-button">
{{- macros_translate::translate(key="load_comments", default="Load Comments", language_strings=language_strings) -}}
</button>
<a class="inline-button colored external" href="https://{{ host }}/@{{ user }}/{{ id }}" rel="{{ rel_attributes }}">
{{- macros_translate::translate(key="open_post", default="Open Post", language_strings=language_strings) -}}
</a>
</div>
<div id="comments-wrapper">
<noscript>
<p>{{ macros_translate::translate(key="comments_noscript", default="Loading comments relies on JavaScript. Try enabling JavaScript and reloading, or visit the original post on Mastodon.", language_strings=language_strings) }}</p>
</noscript>
</div>
</section>
{%- endif -%} {%- endif -%}
{%- if page.lower or page.higher -%} {%- if page.lower or page.higher -%}

View File

@ -18,6 +18,7 @@
<div id="main" class="container"> <div id="main" class="container">
{% block custom %}{% endblock custom %} {% block custom %}{% endblock custom %}
{% block content %}{% endblock content %} {% block content %}{% endblock content %}
{% include "partials/extra_features.html" %}
</div> </div>
{% include "partials/footer.html" %} {% include "partials/footer.html" %}
{% endblock body %} {% endblock body %}

View File

@ -1,381 +0,0 @@
{#- Taken from https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/ -#}
{#- Attachment code taken from https://github.com/cassidyjames/cassidyjames.github.io/blob/99782788a7e3ba3cc52d6803010873abd1b02b9e/_includes/comments.html#L251-L296 -#}
{%- set rel_attributes = macros_rel_attributes::rel_attributes() | trim -%}
{%- if page.extra.comments.host -%}
{%- set host = page.extra.comments.host -%}
{%- else -%}
{%- set host = config.extra.comments.host -%}
{%- endif -%}
{%- if page.extra.comments.user -%}
{%- set user = page.extra.comments.user -%}
{%- else %}
{%- set user = config.extra.comments.user -%}
{%- endif -%}
{%- set id = page.extra.comments.id -%}
{%- set date_locale = macros_translate::translate(key="date_locale", default="en-US", language_strings=language_strings) | replace(from="_", to="-") -%}
<section id="comments">
{%- if config.extra.comments.show_qr -%}
<img
id="qrcode"
class="pixels no-hover"
title="{{ macros_translate::translate(key='comments_qr', default='QR code to a Mastodon post', language_strings=language_strings) }}"
{%- if config.markdown.lazy_async_image -%}
decoding="async"
loading="lazy"
{%- endif -%}
src="https://api.qrserver.com/v1/create-qr-code/?data=https://{{ host }}/@{{ user }}/{{ id }}"
/>
{%- endif -%}
<h2>{{ macros_translate::translate(key="comments", default="Comments", language_strings=language_strings) }}</h2>
<p>{{ macros_translate::translate(key="comments_description", default="You can comment on this blog post by publicly replying to this post using a Mastodon or other ActivityPub/Fediverse account. Known non-private replies are displayed below.", language_strings=language_strings) }}</p>
<div class="dialog-buttons">
<button id="load-comments" class="inline-button" onclick="loadComments()">
{{- macros_translate::translate(key="load_comments", default="Load Comments", language_strings=language_strings) -}}
</button>
<a class="inline-button colored external" href="https://{{ host }}/@{{ user }}/{{ id }}" rel="{{ rel_attributes }}">
{{- macros_translate::translate(key="open_post", default="Open Post", language_strings=language_strings) -}}
</a>
</div>
<div id="comments-wrapper">
<noscript>
<p>{{ macros_translate::translate(key="comments_noscript", default="Loading comments relies on JavaScript. Try enabling JavaScript and reloading, or visit the original post on Mastodon.", language_strings=language_strings) }}</p>
</noscript>
</div>
<script type="text/javascript">
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function emojify(input, emojis) {
let output = input;
emojis.forEach((emoji) => {
let picture = document.createElement("picture");
let source = document.createElement("source");
source.setAttribute("srcset", escapeHtml(emoji.url));
source.setAttribute("media", "(prefers-reduced-motion: no-preference)");
let img = document.createElement("img");
img.className = "emoji";
img.setAttribute("src", escapeHtml(emoji.static_url));
img.setAttribute("alt", `:${emoji.shortcode}:`);
img.setAttribute("title", `:${emoji.shortcode}:`);
{%- if config.markdown.lazy_async_image -%}
img.setAttribute("decoding", "async");
img.setAttribute("loading", "lazy");
{%- endif -%}
picture.appendChild(source);
picture.appendChild(img);
output = output.replace(`:${emoji.shortcode}:`, picture.outerHTML);
});
return output;
}
function loadComments() {
let commentsWrapper = document.getElementById("comments-wrapper");
commentsWrapper.innerHTML = "";
let loadCommentsButton = document.getElementById("load-comments");
loadCommentsButton.innerHTML = "{{ macros_translate::translate(key='loading', default='Loading', language_strings=language_strings) }}…";
loadCommentsButton.disabled = true;
fetch("https://{{ host }}/api/v1/statuses/{{ id }}/context")
.then(function (response) {
return response.json();
})
.then(function (data) {
let descendants = data["descendants"];
if (
descendants &&
Array.isArray(descendants) &&
descendants.length > 0
) {
commentsWrapper.innerHTML = "";
descendants.forEach(function (status) {
console.log(descendants);
if (status.account.display_name.length > 0) {
status.account.display_name = escapeHtml(
status.account.display_name
);
status.account.display_name = emojify(
status.account.display_name,
status.account.emojis
);
} else {
status.account.display_name = status.account.username;
}
let instance = "";
if (status.account.acct.includes("@")) {
instance = status.account.acct.split("@")[1];
} else {
instance = "{{ host }}";
}
const isReply = status.in_reply_to_id !== "{{ id }}";
let op = false;
if (status.account.acct == "{{ user }}") {
op = true;
}
status.content = emojify(status.content, status.emojis);
let avatarSource = document.createElement("source");
avatarSource.setAttribute(
"srcset",
escapeHtml(status.account.avatar)
);
avatarSource.setAttribute(
"media",
"(prefers-reduced-motion: no-preference)"
);
let avatarImg = document.createElement("img");
avatarImg.className = "avatar";
avatarImg.setAttribute(
"src",
escapeHtml(status.account.avatar_static)
);
avatarImg.setAttribute(
"alt",
`@${status.account.username}@${instance} avatar`
);
{%- if config.markdown.lazy_async_image -%}
avatarImg.setAttribute("decoding", "async");
avatarImg.setAttribute("loading", "lazy");
{%- endif -%}
let avatarPicture = document.createElement("picture");
avatarPicture.appendChild(avatarSource);
avatarPicture.appendChild(avatarImg);
let avatar = document.createElement("a");
avatar.className = "avatar-link";
avatar.setAttribute("href", status.account.url);
avatar.setAttribute("rel", "{{ rel_attributes }}");
avatar.setAttribute(
"title",
`{{ macros_translate::translate(key="view_profile", default="View Profile At", language_strings=language_strings) }} @${status.account.username}@${instance}`
);
avatar.appendChild(avatarPicture);
let instanceBadge = document.createElement("a");
instanceBadge.className = "instance";
instanceBadge.setAttribute("href", status.account.url);
instanceBadge.setAttribute(
"title",
`@${status.account.username}@${instance}`
);
instanceBadge.setAttribute("rel", "{{ rel_attributes }}");
instanceBadge.textContent = instance;
let display = document.createElement("span");
display.className = "display";
display.setAttribute("itemprop", "author");
display.setAttribute("itemtype", "http://schema.org/Person");
display.innerHTML = status.account.display_name;
let header = document.createElement("header");
header.className = "author";
header.appendChild(display);
header.appendChild(instanceBadge);
let permalink = document.createElement("a");
permalink.setAttribute("href", status.url);
permalink.setAttribute("itemprop", "url");
permalink.setAttribute("title", `{{ macros_translate::translate(key="view_comment", default="View Comment At", language_strings=language_strings) }} ${instance}`);
permalink.setAttribute("rel", "{{ rel_attributes }}");
permalink.textContent = new Date(
status.created_at
).toLocaleString("{{ date_locale }}", {
dateStyle: "long",
timeStyle: "short",
});
let timestamp = document.createElement("time");
timestamp.setAttribute("datetime", status.created_at);
timestamp.appendChild(permalink);
permalink.classList.add("external");
let main = document.createElement("main");
main.setAttribute("itemprop", "text");
main.innerHTML = status.content;
let attachments = status.media_attachments;
let SUPPORTED_MEDIA = ["image", "video", "gifv", "audio"];
let media = document.createElement("div");
media.className = "attachments";
if (
attachments &&
Array.isArray(attachments) &&
attachments.length > 0
) {
attachments.forEach((attachment) => {
if (SUPPORTED_MEDIA.includes(attachment.type)) {
let mediaElement;
switch (attachment.type) {
case "image":
mediaElement = document.createElement("img");
mediaElement.setAttribute("src", attachment.preview_url);
if (attachment.description != null) {
mediaElement.setAttribute("alt", attachment.description);
mediaElement.setAttribute("title", attachment.description);
}
{%- if config.markdown.lazy_async_image -%}
mediaElement.setAttribute("decoding", "async");
mediaElement.setAttribute("loading", "lazy");
{%- endif -%}
media.appendChild(mediaElement);
break;
case "video":
mediaElement = document.createElement("video");
mediaElement.setAttribute("src", attachment.url);
mediaElement.setAttribute("controls", "");
if (attachment.description != null) {
mediaElement.setAttribute("aria-title", attachment.description);
mediaElement.setAttribute("title", attachment.description);
}
media.appendChild(mediaElement);
break;
case "gifv":
mediaElement = document.createElement("video");
mediaElement.setAttribute("src", attachment.url);
mediaElement.setAttribute("autoplay", "");
mediaElement.setAttribute("playsinline", "");
mediaElement.setAttribute("loop", "");
if (attachment.description != null) {
mediaElement.setAttribute("aria-title", attachment.description);
mediaElement.setAttribute("title", attachment.description);
}
media.appendChild(mediaElement);
break;
case "audio":
mediaElement = document.createElement("audio");
mediaElement.setAttribute("src", attachment.url);
mediaElement.setAttribute("controls", "");
if (attachment.description != null) {
mediaElement.setAttribute("aria-title", attachment.description);
mediaElement.setAttribute("title", attachment.description);
}
media.appendChild(mediaElement);
break;
}
let mediaLink = document.createElement("a");
mediaLink.setAttribute("href", attachment.url);
mediaLink.setAttribute("rel", "{{ rel_attributes }}");
mediaLink.appendChild(mediaElement);
media.appendChild(mediaLink);
}
});
}
let interactions = document.createElement("footer");
let boosts = document.createElement("a");
boosts.className = "boosts";
boosts.setAttribute("href", `${status.url}/reblogs`);
boosts.setAttribute("title", `{{ macros_translate::translate(key="boosts_from", default="Boosts from $INSTANCE", language_strings=language_strings) }}`.replace("$INSTANCE", instance));
let boostsIcon = document.createElement("i");
boostsIcon.className = "icon";
boosts.appendChild(boostsIcon);
boosts.insertAdjacentHTML('beforeend', ` ${status.reblogs_count}`);
interactions.appendChild(boosts);
let faves = document.createElement("a");
faves.className = "faves";
faves.setAttribute("href", `${status.url}/favourites`);
faves.setAttribute("title", `{{ macros_translate::translate(key="faves_from", default="Favorites from $INSTANCE", language_strings=language_strings) }}`.replace("$INSTANCE", instance));
let favesIcon = document.createElement("i");
favesIcon.className = "icon";
faves.appendChild(favesIcon);
faves.insertAdjacentHTML('beforeend', ` ${status.favourites_count}`);
interactions.appendChild(faves);
let comment = document.createElement("article");
comment.id = `comment-${status.id}`;
comment.className = isReply ? "comment comment-reply" : "comment";
comment.setAttribute("itemprop", "comment");
comment.setAttribute("itemtype", "http://schema.org/Comment");
comment.appendChild(avatar);
comment.appendChild(header);
comment.appendChild(timestamp);
comment.appendChild(main);
if (
attachments &&
Array.isArray(attachments) &&
attachments.length > 0
) {
comment.appendChild(media);
}
comment.appendChild(interactions);
if (op === true) {
comment.classList.add("op");
avatar.classList.add("op");
avatar.setAttribute(
"title",
"{{ macros_translate::translate(key='blog_post_author', default='Blog post author', language_strings=language_strings) }}: " + avatar.getAttribute("title")
);
instanceBadge.classList.add("op");
instanceBadge.setAttribute(
"title",
"{{ macros_translate::translate(key='blog_post_author', default='Blog post author', language_strings=language_strings) }}: " + instanceBadge.getAttribute("title")
);
}
commentsWrapper.innerHTML += comment.outerHTML;
});
}
else {
var statusText = document.createElement("p");
statusText.innerHTML = "{{ macros_translate::translate(key='no_comments', default='No Comments yet :/', language_strings=language_strings) }}";
statusText.setAttribute("id", "comments-status");
commentsWrapper.appendChild(statusText);
}
loadCommentsButton.innerHTML = "{{ macros_translate::translate(key='reload', default='Reload', language_strings=language_strings) }}";
})
.catch(function (error) {
console.error('Error loading comments:', error);
})
.finally(function () {
loadCommentsButton.disabled = false;
});
}
</script>
</section>

View File

@ -1,59 +0,0 @@
{#- Based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html -#}
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function () {
let blocks = document.querySelectorAll("pre[class^='language-']");
blocks.forEach((block) => {
if (navigator.clipboard) {
// Code block header title
let title = document.createElement("span");
let lang = block.getAttribute("data-lang");
title.innerHTML = lang;
// Copy button icon
let icon = document.createElement("i");
icon.classList.add("icon");
// Copy button
let button = document.createElement("button");
button.setAttribute("title", "{{ macros_translate::translate(key='copy_code', default='Copy code', language_strings=language_strings) }}")
button.appendChild(icon);
// Code block header
let header = document.createElement("div");
header.classList.add("header");
header.appendChild(title);
header.appendChild(button);
// Container that holds header and the code block itself
let container = document.createElement("div");
container.classList.add("pre-container");
container.appendChild(header);
// Move code block into the container
block.parentNode.insertBefore(container, block);
container.appendChild(block);
button.addEventListener("click", async () => {
await copyCode(block, header, button); // Pass the button here
});
}
});
async function copyCode(block, header, button) {
let code = block.querySelector("code");
let text = code.innerText;
await navigator.clipboard.writeText(text);
header.classList.add("active");
button.setAttribute("disabled", true);
header.addEventListener("animationend", () => {
header.classList.remove("active");
button.removeAttribute("disabled");
}, { once: true });
}
});
</script>

View File

@ -0,0 +1,8 @@
{%- if config.extra.show_copy_button %}
<span id="copy-code-text" class="hidden">{{ macros_translate::translate(key="copy_code", default="Copy Code", language_strings=language_strings) }}</span>
{%- endif %}
{%- if config.build_search_index %}
<span id="search-index" class="hidden">{{ get_url(path="/", lang=lang) }}/search_index.{{ config.default_language }}.json</span>
<span id="more-matches-text" class="hidden">{{ macros_translate::translate(key="more_matches", default="$MATCHES more matches", language_strings=language_strings) }}</span>
{%- endif %}

View File

@ -67,12 +67,12 @@
{%- endif %} {%- endif %}
{%- endif %} {%- endif %}
{%- if config.extra.show_copy_button %}
{%- include "partials/copy_button.html" %}
{%- endif %}
{%- set scripts = ["closable.js"] %} {%- set scripts = ["closable.js"] %}
{%- if config.extra.show_copy_button %}
{%- set scripts = scripts | concat(with=["copy-button.js"]) %}
{%- endif %}
{%- if config.extra.goatcounter %} {%- if config.extra.goatcounter %}
{%- set scripts = scripts | concat(with=["count.js"]) %} {%- set scripts = scripts | concat(with=["count.js"]) %}
{%- endif %} {%- endif %}
@ -93,11 +93,9 @@
{%- if config.build_search_index %} {%- if config.build_search_index %}
{%- if config.search.index_format == "elasticlunr_json" -%} {%- if config.search.index_format == "elasticlunr_json" -%}
{%- include "partials/search_elasticlunr.html" %} {%- set scripts = scripts | concat(with=["elasticlunr.min.js", "search-elasticlunr.js"]) %}
{%- set scripts = scripts | concat(with=["elasticlunr.min.js"]) %}
{%- elif config.search.index_format == "fuse_json" -%} {%- elif config.search.index_format == "fuse_json" -%}
{%- include "partials/search_fuse.html" %} {%- set scripts = scripts | concat(with=["fuse.js", "search-fuse.js"]) %}
{%- set scripts = scripts | concat(with=["fuse.js"]) %}
{%- endif -%} {%- endif -%}
{%- endif %} {%- endif %}
@ -105,6 +103,10 @@
{%- set scripts = scripts | concat(with=["theme-switcher.js"]) %} {%- set scripts = scripts | concat(with=["theme-switcher.js"]) %}
{%- endif %} {%- endif %}
{%- if page.extra.comments.id %}
{%- set scripts = scripts | concat(with=["comments.js"]) %}
{%- endif %}
{%- if config.extra.scripts %} {%- if config.extra.scripts %}
{%- set scripts = scripts | concat(with=config.extra.scripts) %} {%- set scripts = scripts | concat(with=config.extra.scripts) %}
{%- endif %} {%- endif %}

View File

@ -71,7 +71,7 @@
{%- endfor -%} {%- endfor -%}
{%- if config.build_search_index %} {%- if config.build_search_index %}
<li id="search"> <li id="search">
<button class="circle" onclick="toggleSearch()" title="{{ macros_translate::translate(key='search', default='Search', language_strings=language_strings) }}"> <button id="search-toggle" class="circle" title="{{ macros_translate::translate(key='search', default='Search', language_strings=language_strings) }}">
<i class="icon"></i> <i class="icon"></i>
</button> </button>
</li> </li>
@ -87,17 +87,17 @@
</summary> </summary>
<ul> <ul>
<li> <li>
<button class="circle" id="theme-light" onclick="switchTheme('light')" title="{{ macros_translate::translate(key='theme_light', default='Switch to Light Theme', language_strings=language_strings) }}"> <button class="circle" id="theme-light" title="{{ macros_translate::translate(key='theme_light', default='Switch to Light Theme', language_strings=language_strings) }}">
<i class="icon"></i> <i class="icon"></i>
</button> </button>
</li> </li>
<li> <li>
<button class="circle" id="theme-dark" onclick="switchTheme('dark')" title="{{ macros_translate::translate(key='theme_dark', default='Switch to Dark Theme', language_strings=language_strings) }}"> <button class="circle" id="theme-dark" title="{{ macros_translate::translate(key='theme_dark', default='Switch to Dark Theme', language_strings=language_strings) }}">
<i class="icon"></i> <i class="icon"></i>
</button> </button>
</li> </li>
<li> <li>
<button class="circle" id="theme-system" onclick="switchTheme('system')" title="{{ macros_translate::translate(key='theme_system', default='Use System Theme', language_strings=language_strings) }}"> <button class="circle" id="theme-system" title="{{ macros_translate::translate(key='theme_system', default='Use System Theme', language_strings=language_strings) }}">
<i class="icon"></i> <i class="icon"></i>
</button> </button>
</li> </li>
@ -144,7 +144,7 @@
</nav> </nav>
{%- if config.build_search_index %} {%- if config.build_search_index %}
<div id="search-container"> <div id="search-container">
<label for="search-bar" class="hidden"> <label for="search-bar" class="visually-hidden">
{{- macros_translate::translate(key="search", default="Search", language_strings=language_strings) -}} {{- macros_translate::translate(key="search", default="Search", language_strings=language_strings) -}}
</label> </label>
<input id="search-bar" placeholder="{{ macros_translate::translate(key='search_for', default='Search for', language_strings=language_strings) }}…" autocomplete="off" type="search" disabled> <input id="search-bar" placeholder="{{ macros_translate::translate(key='search_for', default='Search for', language_strings=language_strings) }}…" autocomplete="off" type="search" disabled>

View File

@ -1,209 +0,0 @@
{#- Based on https://github.com/getzola/zola/blob/1ac1231de1e342bbaf4d7a51a8a9a40ea152e246/docs/static/search.js -#}
<script type="text/javascript">
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
};
}
// Taken from mdbook
// The strategy is as follows:
// First, assign a value to each word in the document:
// Words that correspond to search terms (stemmer aware): 40
// Normal words: 2
// First word in a sentence: 8
// Then use a sliding window with a constant number of words and count the
// sum of the values of the words within the window. Then use the window that got the
// maximum sum. If there are multiple maximas, then get the last one.
// Enclose the terms in <b>.
function makeTeaser(body, terms) {
var TERM_WEIGHT = 40;
var NORMAL_WORD_WEIGHT = 2;
var FIRST_WORD_WEIGHT = 8;
var TEASER_MAX_WORDS = 30;
var stemmedTerms = terms.map(function (w) {
return elasticlunr.stemmer(w.toLowerCase());
});
var termFound = false;
var index = 0;
var weighted = []; // contains elements of ["word", weight, index_in_document]
// split in sentences, then words
var sentences = body.toLowerCase().split(". ");
for (var i in sentences) {
var words = sentences[i].split(" ");
var value = FIRST_WORD_WEIGHT;
for (var j in words) {
var word = words[j];
if (word.length > 0) {
for (var k in stemmedTerms) {
if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
value = TERM_WEIGHT;
termFound = true;
}
}
weighted.push([word, value, index]);
value = NORMAL_WORD_WEIGHT;
}
index += word.length;
index += 1; // ' ' or '.' if last word in sentence
}
index += 1; // because we split at a two-char boundary '. '
}
if (weighted.length === 0) {
return body;
}
var windowWeights = [];
var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS);
// We add a window with all the weights first
var curSum = 0;
for (var i = 0; i < windowSize; i++) {
curSum += weighted[i][1];
}
windowWeights.push(curSum);
for (var i = 0; i < weighted.length - windowSize; i++) {
curSum -= weighted[i][1];
curSum += weighted[i + windowSize][1];
windowWeights.push(curSum);
}
// If we didn't find the term, just pick the first window
var maxSumIndex = 0;
if (termFound) {
var maxFound = 0;
// backwards
for (var i = windowWeights.length - 1; i >= 0; i--) {
if (windowWeights[i] > maxFound) {
maxFound = windowWeights[i];
maxSumIndex = i;
}
}
}
var teaser = [];
var startIndex = weighted[maxSumIndex][2];
for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
var word = weighted[i];
if (startIndex < word[2]) {
// missing text from index to start of `word`
teaser.push(body.substring(startIndex, word[2]));
startIndex = word[2];
}
// add <strong> around search terms
if (word[1] === TERM_WEIGHT) {
teaser.push("<strong>");
}
startIndex = word[2] + word[0].length;
teaser.push(body.substring(word[2], startIndex));
if (word[1] === TERM_WEIGHT) {
teaser.push("</strong>");
}
}
teaser.push("…");
return teaser.join("");
}
function formatSearchResultItem(item, terms) {
return '<div class="item">'
+ `<a href="${item.ref}">${item.doc.title}</a>`
+ `<span>${makeTeaser(item.doc.body, terms)}</span>`
+ '</div>';
}
function initSearch() {
var searchBar = document.getElementById("search-bar");
var searchContainer = document.getElementById("search-container");
var searchResults = document.getElementById("search-results");
var MAX_ITEMS = 10;
var options = {
bool: "AND",
fields: {
title: { boost: 2 },
body: { boost: 1 },
}
};
var currentTerm = "";
var index;
var initIndex = async function () {
if (index === undefined) {
index = fetch("{{ get_url(path='/', lang=lang) }}/search_index.{{ config.default_language }}.json")
.then(
async function (response) {
return await elasticlunr.Index.load(await response.json());
}
);
}
let res = await index;
return res;
}
searchBar.addEventListener("keyup", debounce(async function () {
var term = searchBar.value.trim();
if (term === currentTerm) {
return;
}
searchResults.style.display = term === "" ? "none" : "flex";
searchResults.innerHTML = "";
currentTerm = term;
if (term === "") {
return;
}
var results = (await initIndex()).search(term, options);
if (results.length === 0) {
searchResults.style.display = "none";
return;
}
for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
searchResults.innerHTML += formatSearchResultItem(results[i], term.split(" "));
}
}, 150));
document.addEventListener("keydown", function(event) {
if (event.key === "/") {
event.preventDefault();
toggleSearch();
}
});
}
function toggleSearch() {
var searchContainer = document.getElementById("search-container");
var searchBar = document.getElementById("search-bar");
searchContainer.classList.toggle("active");
searchBar.toggleAttribute("disabled");
searchBar.focus();
}
if (document.readyState === "complete" ||
(document.readyState !== "loading" && !document.documentElement.doScroll)
) {
initSearch();
} else {
document.addEventListener("DOMContentLoaded", initSearch);
}
</script>