Rewrite half the theme to make Mozilla observatory more happy about strict CSP
This commit is contained in:
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
9
sass/_visually-hidden.scss
Normal file
9
sass/_visually-hidden.scss
Normal 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;
|
||||||
|
}
|
@ -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
349
static/comments.js
Normal 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, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
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
57
static/copy-button.js
Normal 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 });
|
||||||
|
}
|
||||||
|
});
|
209
static/search-elasticlunr.js
Normal file
209
static/search-elasticlunr.js
Normal 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);
|
||||||
|
}
|
@ -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>
|
|
@ -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);
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 -%}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
}
|
|
||||||
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>
|
|
@ -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>
|
|
8
templates/partials/extra_features.html
Normal file
8
templates/partials/extra_features.html
Normal 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 %}
|
@ -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 %}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
Reference in New Issue
Block a user