Rewrite half the theme to make Mozilla observatory more happy about strict CSP
This commit is contained in:
@ -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>
|
||||
|
||||
<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 %}
|
||||
<a class="inline-button colored external" href="{{ config.extra.issues_url }}" rel="{{ rel_attributes }}">File an Issue</a>
|
||||
{%- endif %}
|
||||
|
@ -135,7 +135,62 @@
|
||||
</article>
|
||||
|
||||
{%- 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 -%}
|
||||
|
||||
{%- if page.lower or page.higher -%}
|
||||
|
@ -18,6 +18,7 @@
|
||||
<div id="main" class="container">
|
||||
{% block custom %}{% endblock custom %}
|
||||
{% block content %}{% endblock content %}
|
||||
{% include "partials/extra_features.html" %}
|
||||
</div>
|
||||
{% include "partials/footer.html" %}
|
||||
{% 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 %}
|
||||
|
||||
{%- if config.extra.show_copy_button %}
|
||||
{%- include "partials/copy_button.html" %}
|
||||
{%- endif %}
|
||||
|
||||
{%- set scripts = ["closable.js"] %}
|
||||
|
||||
{%- if config.extra.show_copy_button %}
|
||||
{%- set scripts = scripts | concat(with=["copy-button.js"]) %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if config.extra.goatcounter %}
|
||||
{%- set scripts = scripts | concat(with=["count.js"]) %}
|
||||
{%- endif %}
|
||||
@ -93,11 +93,9 @@
|
||||
|
||||
{%- if config.build_search_index %}
|
||||
{%- if config.search.index_format == "elasticlunr_json" -%}
|
||||
{%- include "partials/search_elasticlunr.html" %}
|
||||
{%- set scripts = scripts | concat(with=["elasticlunr.min.js"]) %}
|
||||
{%- set scripts = scripts | concat(with=["elasticlunr.min.js", "search-elasticlunr.js"]) %}
|
||||
{%- elif config.search.index_format == "fuse_json" -%}
|
||||
{%- include "partials/search_fuse.html" %}
|
||||
{%- set scripts = scripts | concat(with=["fuse.js"]) %}
|
||||
{%- set scripts = scripts | concat(with=["fuse.js", "search-fuse.js"]) %}
|
||||
{%- endif -%}
|
||||
{%- endif %}
|
||||
|
||||
@ -105,6 +103,10 @@
|
||||
{%- set scripts = scripts | concat(with=["theme-switcher.js"]) %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if page.extra.comments.id %}
|
||||
{%- set scripts = scripts | concat(with=["comments.js"]) %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if config.extra.scripts %}
|
||||
{%- set scripts = scripts | concat(with=config.extra.scripts) %}
|
||||
{%- endif %}
|
||||
|
@ -71,7 +71,7 @@
|
||||
{%- endfor -%}
|
||||
{%- if config.build_search_index %}
|
||||
<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>
|
||||
</button>
|
||||
</li>
|
||||
@ -87,17 +87,17 @@
|
||||
</summary>
|
||||
<ul>
|
||||
<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>
|
||||
</button>
|
||||
</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>
|
||||
</button>
|
||||
</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>
|
||||
</button>
|
||||
</li>
|
||||
@ -144,7 +144,7 @@
|
||||
</nav>
|
||||
{%- if config.build_search_index %}
|
||||
<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) -}}
|
||||
</label>
|
||||
<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>
|
@ -1,127 +0,0 @@
|
||||
{#- Based on https://codeberg.org/daudix/duckquill/issues/101#issuecomment-2377169 -#}
|
||||
|
||||
<script type="text/javascript">
|
||||
let searchSetup = false;
|
||||
let fuse;
|
||||
|
||||
async function initIndex() {
|
||||
if (searchSetup) return;
|
||||
|
||||
const url = "{{ get_url(path='/', lang=lang) }}/search_index.{{ config.default_language }}.json";
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
const options = {
|
||||
includeScore: false,
|
||||
includeMatches: true,
|
||||
ignoreLocation: true,
|
||||
threshold: 0.15,
|
||||
keys: [
|
||||
{ name: "title", weight: 3 },
|
||||
{ name: "description", weight: 2 },
|
||||
{ name: "body", weight: 1 }
|
||||
]
|
||||
};
|
||||
|
||||
fuse = new Fuse(await response.json(), options);
|
||||
searchSetup = true;
|
||||
|
||||
console.log("Search index initialized successfully");
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
initIndex();
|
||||
const searchBar = document.getElementById("search-bar");
|
||||
const searchContainer = document.getElementById("search-container");
|
||||
const searchResults = document.getElementById("search-results");
|
||||
searchContainer.classList.toggle("active");
|
||||
searchBar.toggleAttribute("disabled");
|
||||
searchBar.focus();
|
||||
}
|
||||
|
||||
function debounce(actual_fn, wait) {
|
||||
let timeoutId;
|
||||
|
||||
return (...args) => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
actual_fn(...args);
|
||||
}, wait);
|
||||
};
|
||||
};
|
||||
|
||||
function initSearch() {
|
||||
const searchBar = document.getElementById("search-bar");
|
||||
const searchResults = document.getElementById("search-results");
|
||||
const searchContainer = document.getElementById("search-container");
|
||||
const MAX_ITEMS = 10;
|
||||
const MAX_RESULTS = 4;
|
||||
|
||||
let currentTerm = "";
|
||||
|
||||
searchBar.addEventListener("keyup", (e) => {
|
||||
const searchVal = searchBar.value.trim();
|
||||
const results = fuse.search(searchVal, { limit: MAX_ITEMS });
|
||||
|
||||
let html = "";
|
||||
for (const result of results) {
|
||||
html += makeTeaser(result, searchVal);
|
||||
}
|
||||
searchResults.innerHTML = html;
|
||||
|
||||
if (html) {
|
||||
searchResults.style.display = "flex";
|
||||
} else {
|
||||
searchResults.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
function makeTeaser(result, searchVal) {
|
||||
const TEASER_SIZE = 20;
|
||||
let output = `<div class="search-result item"><a class="result-title" href=${result.item.url}>${result.item.title}</a>`;
|
||||
|
||||
for (const match of result.matches) {
|
||||
if (match.key === "title") continue;
|
||||
|
||||
const indices = match.indices.sort((a, b) => Math.abs(a[1] - a[0] - searchVal.length) - Math.abs(b[1] - b[0] - searchVal.length)).slice(0, MAX_RESULTS);
|
||||
const value = match.value;
|
||||
|
||||
for (const ind of indices) {
|
||||
const start = Math.max(0, ind[0] - TEASER_SIZE);
|
||||
const end = Math.min(value.length - 1, ind[1] + TEASER_SIZE);
|
||||
output += "<span>"
|
||||
+ value.substring(start, ind[0])
|
||||
+ `<strong>${value.substring(ind[0], ind[1] + 1)}</strong>`
|
||||
+ value.substring(ind[1] + 1, end)
|
||||
+ "</span>";
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
return output + "</div>";
|
||||
}
|
||||
|
||||
/*window.addEventListener("click", function (event) {
|
||||
if (searchSetup && searchBar.getAttribute("disabled") === null && !searchContainer.contains(event.target)) {
|
||||
toggleSearch();
|
||||
}
|
||||
}, { passive: true });*/
|
||||
|
||||
document.addEventListener("keydown", function(event) {
|
||||
if (event.key === "/") {
|
||||
event.preventDefault();
|
||||
toggleSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "complete" ||
|
||||
(document.readyState !== "loading" && !document.documentElement.doScroll))
|
||||
initSearch();
|
||||
else
|
||||
document.addEventListener("DOMContentLoaded", initSearch);
|
||||
</script>
|
Reference in New Issue
Block a user