First commit
Some checks failed
Blowfish Docs Deploy / build (push) Has been cancelled
Blowfish Docs Deploy / deploy (push) Has been cancelled
Test Build / Build Example Site (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Update Hugo version / updateBlowfish (push) Has been cancelled
Some checks failed
Blowfish Docs Deploy / build (push) Has been cancelled
Blowfish Docs Deploy / deploy (push) Has been cancelled
Test Build / Build Example Site (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Update Hugo version / updateBlowfish (push) Has been cancelled
Delete exampleSite Add initial content, images & docker-compose.yml Use extend-head.html for analytics Set remote url to gitea.novicelab.io Remove original .git due to "shallow update not allowed" error
This commit is contained in:
170
assets/js/a11y.js
Normal file
170
assets/js/a11y.js
Normal file
@@ -0,0 +1,170 @@
|
||||
window.A11yPanel = (() => {
|
||||
const FEATURES = {
|
||||
disableBlur: {
|
||||
default: false,
|
||||
apply: (enabled) => {
|
||||
document.querySelectorAll("script[data-blur-id]").forEach((script) => {
|
||||
const targetId = script.getAttribute("data-blur-id");
|
||||
const scrollDivisor = Number(script.getAttribute("data-scroll-divisor") || 300);
|
||||
if (typeof setBackgroundBlur === "function") {
|
||||
setBackgroundBlur(targetId, scrollDivisor, enabled, targetId === "menu-blur");
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
disableImages: {
|
||||
default: false,
|
||||
apply: (enabled) => {
|
||||
const image = document.getElementById("background-image");
|
||||
if (image) {
|
||||
image.style.display = enabled ? "none" : "";
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
fontSize: {
|
||||
default: "default",
|
||||
apply: (size) => {
|
||||
document.documentElement.style.fontSize = size === "default" ? "" : size;
|
||||
},
|
||||
},
|
||||
|
||||
underlineLinks: {
|
||||
default: false,
|
||||
apply: (enabled) => {
|
||||
const existing = document.getElementById("a11y-underline-links");
|
||||
if (enabled && !existing) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "a11y-underline-links";
|
||||
style.textContent = `
|
||||
a { text-decoration: underline !important; }
|
||||
.group-hover-card-title { text-decoration: underline !important; }
|
||||
.group-hover-card:hover .group-hover-card-title { text-decoration: underline !important; }`;
|
||||
document.head.appendChild(style);
|
||||
} else if (!enabled && existing) {
|
||||
existing.remove();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
zenMode: {
|
||||
default: false,
|
||||
apply: (enabled) => {
|
||||
const isActive = document.body?.classList.contains("zen-mode-enable");
|
||||
if (enabled !== isActive) {
|
||||
const checkbox = document.querySelector('[id$="zen-mode"]');
|
||||
if (checkbox && typeof _toggleZenMode === "function") {
|
||||
_toggleZenMode(checkbox, { scrollToHeader: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let settings = null;
|
||||
|
||||
const getSettings = () => {
|
||||
if (settings) return settings;
|
||||
const defaults = Object.fromEntries(Object.entries(FEATURES).map(([key, config]) => [key, config.default]));
|
||||
try {
|
||||
const saved = localStorage.getItem("a11ySettings");
|
||||
settings = { ...defaults, ...JSON.parse(saved || "{}") };
|
||||
} catch {
|
||||
settings = defaults;
|
||||
}
|
||||
return settings;
|
||||
};
|
||||
|
||||
const updateSetting = (key, value) => {
|
||||
const current = getSettings();
|
||||
current[key] = value;
|
||||
try {
|
||||
localStorage.setItem("a11ySettings", JSON.stringify(current));
|
||||
} catch (e) {
|
||||
console.warn(`a11y.js: can not store settings: ${e}`);
|
||||
}
|
||||
FEATURES[key]?.apply(value);
|
||||
};
|
||||
|
||||
const initPanel = (panelId) => {
|
||||
const prefix = panelId.replace("a11y-panel", "");
|
||||
const current = getSettings();
|
||||
|
||||
Object.entries(FEATURES).forEach(([key, config]) => {
|
||||
const elementId = `${prefix}${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
|
||||
const element = document.getElementById(elementId) || document.getElementById(`${elementId}-select`);
|
||||
|
||||
if (element) {
|
||||
if (element.type === "checkbox") {
|
||||
element.checked = current[key];
|
||||
element.onchange = (e) => updateSetting(key, e.target.checked);
|
||||
} else if (element.tagName === "SELECT") {
|
||||
element.value = current[key];
|
||||
element.onchange = (e) => updateSetting(key, e.target.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const togglePanel = () => {
|
||||
const panel = document.getElementById(panelId);
|
||||
const overlay = document.getElementById(`${prefix}a11y-overlay`);
|
||||
const toggle = document.getElementById(`${prefix}a11y-toggle`);
|
||||
|
||||
if (!panel || !overlay) return;
|
||||
|
||||
const isHidden = overlay.classList.contains("hidden");
|
||||
overlay.classList.toggle("hidden");
|
||||
panel.classList.toggle("hidden");
|
||||
|
||||
if (toggle) {
|
||||
toggle.setAttribute("aria-pressed", String(isHidden));
|
||||
toggle.setAttribute("aria-expanded", String(isHidden));
|
||||
}
|
||||
};
|
||||
|
||||
const toggle = document.getElementById(`${prefix}a11y-toggle`);
|
||||
const close = document.getElementById(`${prefix}a11y-close`);
|
||||
const overlay = document.getElementById(`${prefix}a11y-overlay`);
|
||||
|
||||
if (toggle) toggle.onclick = togglePanel;
|
||||
if (close) close.onclick = togglePanel;
|
||||
if (overlay) overlay.onclick = (e) => e.target === overlay && togglePanel();
|
||||
};
|
||||
|
||||
const applyAll = () => {
|
||||
const current = getSettings();
|
||||
Object.entries(current).forEach(([key, value]) => {
|
||||
FEATURES[key]?.apply(value);
|
||||
});
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
applyAll();
|
||||
document.querySelectorAll('[id$="a11y-panel"]').forEach((panel) => {
|
||||
initPanel(panel.id);
|
||||
});
|
||||
};
|
||||
|
||||
if (getSettings().disableImages) {
|
||||
new MutationObserver(() => {
|
||||
const img = document.getElementById("background-image");
|
||||
if (img) img.style.display = "none";
|
||||
}).observe(document, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
return {
|
||||
getSettings,
|
||||
updateSetting,
|
||||
addFeature: (name, config) => {
|
||||
FEATURES[name] = config;
|
||||
FEATURES[name].apply(getSettings()[name] || config.default);
|
||||
},
|
||||
};
|
||||
})();
|
||||
144
assets/js/appearance.js
Normal file
144
assets/js/appearance.js
Normal file
@@ -0,0 +1,144 @@
|
||||
const sitePreference = document.documentElement.getAttribute("data-default-appearance");
|
||||
const userPreference = localStorage.getItem("appearance");
|
||||
|
||||
if ((sitePreference === "dark" && userPreference === null) || userPreference === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
|
||||
if (document.documentElement.getAttribute("data-auto-appearance") === "true") {
|
||||
if (
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches &&
|
||||
userPreference !== "light"
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => {
|
||||
if (event.matches) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Mermaid dark mode support
|
||||
var updateMermaidTheme = () => {
|
||||
if (typeof mermaid !== 'undefined') {
|
||||
const isDark = document.documentElement.classList.contains("dark");
|
||||
|
||||
const mermaids = document.querySelectorAll('pre.mermaid');
|
||||
mermaids.forEach(e => {
|
||||
if (e.getAttribute('data-processed')) {
|
||||
// Already rendered, clean the processed attributes
|
||||
e.removeAttribute('data-processed');
|
||||
// Replace the rendered HTML with the stored text
|
||||
e.innerHTML = e.getAttribute('data-graph');
|
||||
} else {
|
||||
// First time, store the text
|
||||
e.setAttribute('data-graph', e.textContent);
|
||||
}
|
||||
});
|
||||
|
||||
if (isDark) {
|
||||
initMermaidDark();
|
||||
mermaid.run();
|
||||
} else {
|
||||
initMermaidLight();
|
||||
mermaid.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", (event) => {
|
||||
const switcher = document.getElementById("appearance-switcher");
|
||||
const switcherMobile = document.getElementById("appearance-switcher-mobile");
|
||||
|
||||
updateMeta();
|
||||
this.updateLogo?.(getTargetAppearance());
|
||||
|
||||
// Initialize mermaid theme on page load
|
||||
updateMermaidTheme();
|
||||
|
||||
if (switcher) {
|
||||
switcher.addEventListener("click", () => {
|
||||
document.documentElement.classList.toggle("dark");
|
||||
var targetAppearance = getTargetAppearance();
|
||||
localStorage.setItem(
|
||||
"appearance",
|
||||
targetAppearance
|
||||
);
|
||||
updateMeta();
|
||||
updateMermaidTheme();
|
||||
this.updateLogo?.(targetAppearance);
|
||||
});
|
||||
switcher.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
localStorage.removeItem("appearance");
|
||||
});
|
||||
}
|
||||
if (switcherMobile) {
|
||||
switcherMobile.addEventListener("click", () => {
|
||||
document.documentElement.classList.toggle("dark");
|
||||
var targetAppearance = getTargetAppearance();
|
||||
localStorage.setItem(
|
||||
"appearance",
|
||||
targetAppearance
|
||||
);
|
||||
updateMeta();
|
||||
updateMermaidTheme();
|
||||
this.updateLogo?.(targetAppearance);
|
||||
});
|
||||
switcherMobile.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
localStorage.removeItem("appearance");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var updateMeta = () => {
|
||||
var elem, style;
|
||||
elem = document.querySelector('body');
|
||||
style = getComputedStyle(elem);
|
||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', style.backgroundColor);
|
||||
}
|
||||
|
||||
{{ if and (.Site.Params.Logo) (.Site.Params.SecondaryLogo) }}
|
||||
{{ $primaryLogo := resources.Get .Site.Params.Logo }}
|
||||
{{ $secondaryLogo := resources.Get .Site.Params.SecondaryLogo }}
|
||||
{{ if and ($primaryLogo) ($secondaryLogo) }}
|
||||
var updateLogo = (targetAppearance) => {
|
||||
var imgElems = document.querySelectorAll("img.logo");
|
||||
var logoContainers = document.querySelectorAll("span.logo");
|
||||
|
||||
targetLogoPath =
|
||||
targetAppearance == "{{ .Site.Params.DefaultAppearance }}" ?
|
||||
"{{ $primaryLogo.RelPermalink }}" : "{{ $secondaryLogo.RelPermalink }}"
|
||||
for (const elem of imgElems) {
|
||||
elem.setAttribute("src", targetLogoPath)
|
||||
}
|
||||
|
||||
{{ if eq $primaryLogo.MediaType.SubType "svg" }}
|
||||
targetContent =
|
||||
targetAppearance == "{{ .Site.Params.DefaultAppearance }}" ?
|
||||
`{{ $primaryLogo.Content | safeHTML }}` : `{{ $secondaryLogo.Content | safeHTML }}`
|
||||
for (const container of logoContainers) {
|
||||
container.innerHTML = targetContent;
|
||||
}
|
||||
{{ end }}
|
||||
}
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
|
||||
var getTargetAppearance = () => {
|
||||
return document.documentElement.classList.contains("dark") ? "dark" : "light"
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", (event) => {
|
||||
const scroller = document.getElementById("top-scroller");
|
||||
const footer = document.getElementById("site-footer");
|
||||
if(scroller && footer && scroller.getBoundingClientRect().top > footer.getBoundingClientRect().top) {
|
||||
scroller.hidden = true;
|
||||
}
|
||||
});
|
||||
39
assets/js/background-blur.js
Normal file
39
assets/js/background-blur.js
Normal file
@@ -0,0 +1,39 @@
|
||||
function setBackgroundBlur(targetId, scrollDivisor = 300, disableBlur = false, isMenuBlur = false) {
|
||||
if (!targetId) {
|
||||
console.error("data-blur-id is null");
|
||||
return;
|
||||
}
|
||||
const blurElement = document.getElementById(targetId);
|
||||
if (!blurElement) return;
|
||||
if (disableBlur) {
|
||||
blurElement.setAttribute("aria-hidden", "true");
|
||||
if (!isMenuBlur) {
|
||||
blurElement.style.display = "none";
|
||||
blurElement.style.opacity = "0";
|
||||
} else {
|
||||
blurElement.style.display = "";
|
||||
}
|
||||
} else {
|
||||
blurElement.style.display = "";
|
||||
blurElement.removeAttribute("aria-hidden");
|
||||
}
|
||||
const updateBlur = () => {
|
||||
if (!disableBlur || isMenuBlur) {
|
||||
const scroll = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
blurElement.style.opacity = scroll / scrollDivisor;
|
||||
}
|
||||
};
|
||||
blurElement.setAttribute("role", "presentation");
|
||||
blurElement.setAttribute("tabindex", "-1");
|
||||
window.addEventListener("scroll", updateBlur);
|
||||
updateBlur();
|
||||
}
|
||||
|
||||
document.querySelectorAll("script[data-blur-id]").forEach((script) => {
|
||||
const targetId = script.getAttribute("data-blur-id");
|
||||
const scrollDivisor = Number(script.getAttribute("data-scroll-divisor") || 300);
|
||||
const isMenuBlur = targetId === "menu-blur";
|
||||
const settings = JSON.parse(localStorage.getItem("a11ySettings") || "{}");
|
||||
const disableBlur = settings.disableBlur || false;
|
||||
setBackgroundBlur(targetId, scrollDivisor, disableBlur, isMenuBlur);
|
||||
});
|
||||
13
assets/js/chart.js
Normal file
13
assets/js/chart.js
Normal file
@@ -0,0 +1,13 @@
|
||||
function css(name) {
|
||||
return "rgb(" + getComputedStyle(document.documentElement).getPropertyValue(name) + ")";
|
||||
}
|
||||
|
||||
Chart.defaults.font.size = 14;
|
||||
Chart.defaults.backgroundColor = css("--color-primary-300");
|
||||
Chart.defaults.elements.point.borderColor = css("--color-primary-400");
|
||||
Chart.defaults.elements.bar.borderColor = css("--color-primary-500");
|
||||
Chart.defaults.elements.bar.borderWidth = 1;
|
||||
Chart.defaults.elements.line.borderColor = css("--color-primary-400");
|
||||
Chart.defaults.elements.arc.backgroundColor = css("--color-primary-200");
|
||||
Chart.defaults.elements.arc.borderColor = css("--color-primary-500");
|
||||
Chart.defaults.elements.arc.borderWidth = 1;
|
||||
78
assets/js/code.js
Normal file
78
assets/js/code.js
Normal file
@@ -0,0 +1,78 @@
|
||||
var scriptBundle = document.getElementById("script-bundle");
|
||||
var copyText = scriptBundle?.getAttribute("data-copy") || "Copy";
|
||||
var copiedText = scriptBundle?.getAttribute("data-copied") || "Copied";
|
||||
|
||||
function createCopyButton(highlightWrapper) {
|
||||
const button = document.createElement("button");
|
||||
button.className = "copy-button";
|
||||
button.type = "button";
|
||||
button.ariaLabel = copyText;
|
||||
button.innerText = copyText;
|
||||
button.addEventListener("click", () => copyCodeToClipboard(button, highlightWrapper));
|
||||
highlightWrapper.insertBefore(button, highlightWrapper.firstChild);
|
||||
}
|
||||
|
||||
async function copyCodeToClipboard(button, highlightWrapper) {
|
||||
const codeToCopy = getCodeText(highlightWrapper);
|
||||
|
||||
function fallback(codeToCopy, highlightWrapper) {
|
||||
const textArea = document.createElement("textArea");
|
||||
textArea.contentEditable = "true";
|
||||
textArea.readOnly = "false";
|
||||
textArea.className = "copy-textarea";
|
||||
textArea.value = codeToCopy;
|
||||
highlightWrapper.insertBefore(textArea, highlightWrapper.firstChild);
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(textArea);
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
textArea.focus();
|
||||
textArea.setSelectionRange(0, 999999);
|
||||
document.execCommand("copy");
|
||||
highlightWrapper.removeChild(textArea);
|
||||
}
|
||||
|
||||
try {
|
||||
result = await navigator.permissions.query({ name: "clipboard-write" });
|
||||
if (result.state == "granted" || result.state == "prompt") {
|
||||
await navigator.clipboard.writeText(codeToCopy);
|
||||
} else {
|
||||
fallback(codeToCopy, highlightWrapper);
|
||||
}
|
||||
} catch (_) {
|
||||
fallback(codeToCopy, highlightWrapper);
|
||||
} finally {
|
||||
button.blur();
|
||||
button.innerText = copiedText;
|
||||
setTimeout(function () {
|
||||
button.innerText = copyText;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function getCodeText(highlightWrapper) {
|
||||
const highlightDiv = highlightWrapper.querySelector(".highlight");
|
||||
if (!highlightDiv) return "";
|
||||
|
||||
const codeBlock = highlightDiv.querySelector("code");
|
||||
const inlineLines = codeBlock?.querySelectorAll(".cl"); // linenos=inline
|
||||
const tableCodeCell = highlightDiv?.querySelector(".lntable .lntd:last-child code"); // linenos=table
|
||||
|
||||
if (!codeBlock) return "";
|
||||
|
||||
if (inlineLines.length > 0) {
|
||||
const cleanedLines = Array.from(inlineLines).map((line) => line.textContent.replace(/\n$/, ""));
|
||||
return cleanedLines.join("\n");
|
||||
}
|
||||
|
||||
if (tableCodeCell) {
|
||||
return tableCodeCell.textContent.trim();
|
||||
}
|
||||
|
||||
return codeBlock.textContent.trim();
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", (event) => {
|
||||
document.querySelectorAll(".highlight-wrapper").forEach((highlightWrapper) => createCopyButton(highlightWrapper));
|
||||
});
|
||||
92
assets/js/fetch-repo.js
Normal file
92
assets/js/fetch-repo.js
Normal file
@@ -0,0 +1,92 @@
|
||||
(async () => {
|
||||
const script = document.currentScript;
|
||||
const repoURL = script?.getAttribute("data-repo-url");
|
||||
const repoId = script?.getAttribute("data-repo-id");
|
||||
|
||||
if (!repoURL || !repoId) return;
|
||||
if (repoId.startsWith("forgejo")) {
|
||||
console.log(
|
||||
"fetch-repo.js: Forgejo server blocks cross-origin requests. Live JavaScript updates are not supported.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const platforms = {
|
||||
github: {
|
||||
full_name: "full_name",
|
||||
description: "description",
|
||||
stargazers_count: "stargazers",
|
||||
forks: "forks",
|
||||
},
|
||||
gitlab: {
|
||||
name_with_namespace: "name_with_namespace",
|
||||
description: "description",
|
||||
star_count: "star_count",
|
||||
forks_count: "forks_count",
|
||||
},
|
||||
gitea: {
|
||||
full_name: "full_name",
|
||||
description: "description",
|
||||
stars_count: "stars_count",
|
||||
forks_count: "forks_count",
|
||||
},
|
||||
codeberg: {
|
||||
full_name: "full_name",
|
||||
description: "description",
|
||||
stars_count: "stars_count",
|
||||
forks_count: "forks_count",
|
||||
},
|
||||
forgejo: {
|
||||
full_name: "full_name",
|
||||
description: "description",
|
||||
stars_count: "stars_count",
|
||||
forks_count: "forks_count",
|
||||
},
|
||||
huggingface: {
|
||||
description: "description",
|
||||
likes: "likes",
|
||||
downloads: "downloads",
|
||||
},
|
||||
};
|
||||
|
||||
const processors = {
|
||||
huggingface: {
|
||||
description: (value) => value?.replace(/Dataset Card for .+?\s+Dataset Summary\s+/, "").trim() || value,
|
||||
},
|
||||
};
|
||||
|
||||
const platform = Object.keys(platforms).find((p) => repoId.startsWith(p)) || "github";
|
||||
const mapping = platforms[platform];
|
||||
|
||||
try {
|
||||
const response = await fetch(repoURL, {
|
||||
headers: { "User-agent": "Mozilla/4.0 Custom User Agent" },
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`fetch-repo.js: HTTP Error: ${response.status} ${response.statusText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data || typeof data !== "object") {
|
||||
console.error("fetch-repo.js: Invalid or empty data received from remote");
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(mapping).forEach(([dataField, elementSuffix]) => {
|
||||
const element = document.getElementById(`${repoId}-${elementSuffix}`);
|
||||
if (element) {
|
||||
let value = data[dataField];
|
||||
if (processors[platform]?.[dataField]) {
|
||||
value = processors[platform][dataField](value);
|
||||
}
|
||||
if (value != null && value !== "") {
|
||||
element.innerHTML = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`fetch-repo.js: ${error}`);
|
||||
}
|
||||
})();
|
||||
154
assets/js/firebase.js
Normal file
154
assets/js/firebase.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.23.0/firebase-app.js";
|
||||
import {
|
||||
getFirestore,
|
||||
doc,
|
||||
getDoc,
|
||||
setDoc,
|
||||
updateDoc,
|
||||
increment,
|
||||
onSnapshot,
|
||||
} from "https://www.gstatic.com/firebasejs/9.23.0/firebase-firestore.js";
|
||||
import { getAuth, signInAnonymously } from "https://www.gstatic.com/firebasejs/9.23.0/firebase-auth.js";
|
||||
|
||||
let app, db, auth, oids;
|
||||
try {
|
||||
const configEl = document.getElementById("firebase-config");
|
||||
if (!configEl?.textContent) {
|
||||
throw new Error("Firebase config element not found");
|
||||
}
|
||||
|
||||
const data = JSON.parse(configEl.textContent);
|
||||
app = initializeApp(data.config);
|
||||
|
||||
oids = {
|
||||
views: configEl.getAttribute("data-views"),
|
||||
likes: configEl.getAttribute("data-likes"),
|
||||
};
|
||||
|
||||
db = getFirestore(app);
|
||||
auth = getAuth(app);
|
||||
} catch (e) {
|
||||
console.error("Firebase initialization failed:", e.message);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const id = oids?.views?.replaceAll("/", "-");
|
||||
const id_likes = oids?.likes?.replaceAll("/", "-");
|
||||
let liked = false;
|
||||
let authReady = false;
|
||||
|
||||
function formatNumber(n) {
|
||||
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function toggleLoaders(node) {
|
||||
var classesString = node.className;
|
||||
if (classesString == "") return;
|
||||
var classes = classesString.split(" ");
|
||||
for (var i in classes) {
|
||||
node.classList.toggle(classes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function updateDisplay(collection, nodeId) {
|
||||
const node = document.getElementById(nodeId);
|
||||
if (!node) return;
|
||||
|
||||
const docId = nodeId.replaceAll("/", "-");
|
||||
onSnapshot(
|
||||
doc(db, collection, docId),
|
||||
(snapshot) => {
|
||||
node.innerText = snapshot.exists() ? formatNumber(snapshot.data()[collection]) : 0;
|
||||
toggleLoaders(node);
|
||||
},
|
||||
(error) => {
|
||||
console.error("Firebase snapshot update failed:", error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function recordView(id) {
|
||||
if (!id || localStorage.getItem(id)) return;
|
||||
|
||||
try {
|
||||
const ref = doc(db, "views", id);
|
||||
const snap = await getDoc(ref);
|
||||
|
||||
snap.exists() ? await updateDoc(ref, { views: increment(1) }) : await setDoc(ref, { views: 1 });
|
||||
|
||||
localStorage.setItem(id, true);
|
||||
} catch (e) {
|
||||
console.error("Record view operation failed:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function updateButton(isLiked) {
|
||||
const hearts = document.querySelectorAll("span[id='button_likes_heart']");
|
||||
const empties = document.querySelectorAll("span[id='button_likes_emtpty_heart']");
|
||||
const texts = document.querySelectorAll("span[id='button_likes_text']");
|
||||
|
||||
hearts.forEach((el) => {
|
||||
el.style.display = isLiked ? "" : "none";
|
||||
});
|
||||
empties.forEach((el) => {
|
||||
el.style.display = isLiked ? "none" : "";
|
||||
});
|
||||
texts.forEach((el) => {
|
||||
el.innerText = isLiked ? "" : "\xa0Like";
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleLike(add) {
|
||||
if (!id_likes || !authReady) return;
|
||||
|
||||
try {
|
||||
const ref = doc(db, "likes", id_likes);
|
||||
const snap = await getDoc(ref);
|
||||
|
||||
liked = add;
|
||||
add ? localStorage.setItem(id_likes, true) : localStorage.removeItem(id_likes);
|
||||
updateButton(add);
|
||||
|
||||
snap.exists()
|
||||
? await updateDoc(ref, { likes: increment(add ? 1 : -1) })
|
||||
: await setDoc(ref, { likes: add ? 1 : 0 });
|
||||
} catch (e) {
|
||||
console.error("Like operation failed:", e.message);
|
||||
liked = !add;
|
||||
add ? localStorage.removeItem(id_likes) : localStorage.setItem(id_likes, true);
|
||||
updateButton(!add);
|
||||
}
|
||||
}
|
||||
|
||||
signInAnonymously(auth)
|
||||
.then(() => {
|
||||
authReady = true;
|
||||
|
||||
document.querySelectorAll("span[id^='views_']").forEach((node) => {
|
||||
if (node.id) updateDisplay("views", node.id);
|
||||
});
|
||||
|
||||
document.querySelectorAll("span[id^='likes_']").forEach((node) => {
|
||||
if (node.id) updateDisplay("likes", node.id);
|
||||
});
|
||||
|
||||
recordView(id);
|
||||
|
||||
if (id_likes && localStorage.getItem(id_likes)) {
|
||||
liked = true;
|
||||
updateButton(true);
|
||||
}
|
||||
|
||||
const likeButton = document.getElementById("button_likes");
|
||||
if (likeButton) {
|
||||
likeButton.addEventListener("click", () => {
|
||||
toggleLike(!liked);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Firebase anonymous sign-in failed:", error.message);
|
||||
authReady = false;
|
||||
});
|
||||
|
||||
window.process_article = () => toggleLike(!liked);
|
||||
4
assets/js/katex-render.js
Normal file
4
assets/js/katex-render.js
Normal file
@@ -0,0 +1,4 @@
|
||||
document.getElementById("katex-render") &&
|
||||
document.getElementById("katex-render").addEventListener("load", () => {
|
||||
renderMathInElement(document.body);
|
||||
});
|
||||
33
assets/js/mermaid.js
Normal file
33
assets/js/mermaid.js
Normal file
@@ -0,0 +1,33 @@
|
||||
function css(name) {
|
||||
return "rgb(" + getComputedStyle(document.documentElement).getPropertyValue(name) + ")";
|
||||
}
|
||||
|
||||
function initMermaidLight() {
|
||||
mermaid.initialize({
|
||||
theme: "base",
|
||||
themeVariables: {
|
||||
background: css("--color-neutral"),
|
||||
primaryColor: css("--color-primary-200"),
|
||||
secondaryColor: css("--color-secondary-200"),
|
||||
tertiaryColor: css("--color-neutral-100"),
|
||||
primaryBorderColor: css("--color-primary-400"),
|
||||
secondaryBorderColor: css("--color-secondary-400"),
|
||||
tertiaryBorderColor: css("--color-neutral-400"),
|
||||
lineColor: css("--color-neutral-600"),
|
||||
fontFamily:
|
||||
"ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif",
|
||||
fontSize: "16px",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initMermaidDark() {
|
||||
mermaid.initialize({
|
||||
theme: "dark",
|
||||
themeVariables: {
|
||||
fontFamily:
|
||||
"ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif",
|
||||
fontSize: "16px",
|
||||
},
|
||||
});
|
||||
}
|
||||
17
assets/js/print-support.js
Normal file
17
assets/js/print-support.js
Normal file
@@ -0,0 +1,17 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
var closedDetails = [];
|
||||
window.addEventListener('beforeprint', function() {
|
||||
var allDetails = document.querySelectorAll('details:not([open])');
|
||||
for (var i = 0; i < allDetails.length; i++) {
|
||||
allDetails[i].open = true;
|
||||
closedDetails.push(allDetails[i]);
|
||||
}
|
||||
});
|
||||
window.addEventListener('afterprint', function() {
|
||||
for (var i = 0; i < closedDetails.length; i++) {
|
||||
closedDetails[i].open = false;
|
||||
}
|
||||
closedDetails = [];
|
||||
});
|
||||
})();
|
||||
3
assets/js/rtl.js
Normal file
3
assets/js/rtl.js
Normal file
@@ -0,0 +1,3 @@
|
||||
window.addEventListener("DOMContentLoaded", (event) => {
|
||||
document.querySelectorAll("pre, .highlight-wrapper").forEach((tag) => (tag.dir = "auto"));
|
||||
});
|
||||
13
assets/js/scroll-to-top.js
Normal file
13
assets/js/scroll-to-top.js
Normal file
@@ -0,0 +1,13 @@
|
||||
function scrollToTop() {
|
||||
const scrollToTop = document.getElementById("scroll-to-top");
|
||||
if (window.scrollY > window.innerHeight * 0.5) {
|
||||
scrollToTop.classList.remove("translate-y-4", "opacity-0");
|
||||
scrollToTop.classList.add("translate-y-0", "opacity-100");
|
||||
} else {
|
||||
scrollToTop.classList.remove("translate-y-0", "opacity-100");
|
||||
scrollToTop.classList.add("translate-y-4", "opacity-0");
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", scrollToTop);
|
||||
window.addEventListener("load", scrollToTop);
|
||||
200
assets/js/search.js
Normal file
200
assets/js/search.js
Normal file
@@ -0,0 +1,200 @@
|
||||
var fuse;
|
||||
var showButton = document.getElementById("search-button");
|
||||
var showButtonMobile = document.getElementById("search-button-mobile");
|
||||
var hideButton = document.getElementById("close-search-button");
|
||||
var wrapper = document.getElementById("search-wrapper");
|
||||
var modal = document.getElementById("search-modal");
|
||||
var input = document.getElementById("search-query");
|
||||
var output = document.getElementById("search-results");
|
||||
var first = output.firstChild;
|
||||
var last = output.lastChild;
|
||||
var searchVisible = false;
|
||||
var indexed = false;
|
||||
var hasResults = false;
|
||||
|
||||
// Listen for events
|
||||
showButton ? showButton.addEventListener("click", displaySearch) : null;
|
||||
showButtonMobile ? showButtonMobile.addEventListener("click", displaySearch) : null;
|
||||
hideButton.addEventListener("click", hideSearch);
|
||||
wrapper.addEventListener("click", hideSearch);
|
||||
modal.addEventListener("click", function (event) {
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
return false;
|
||||
});
|
||||
document.addEventListener("keydown", function (event) {
|
||||
// Forward slash to open search wrapper
|
||||
if (event.key == "/") {
|
||||
const active = document.activeElement;
|
||||
const tag = active.tagName;
|
||||
const isInputField = tag === "INPUT" || tag === "TEXTAREA" || active.isContentEditable;
|
||||
|
||||
if (!searchVisible && !isInputField) {
|
||||
event.preventDefault();
|
||||
displaySearch();
|
||||
}
|
||||
}
|
||||
|
||||
// Esc to close search wrapper
|
||||
if (event.key == "Escape") {
|
||||
hideSearch();
|
||||
}
|
||||
|
||||
// Down arrow to move down results list
|
||||
if (event.key == "ArrowDown") {
|
||||
if (searchVisible && hasResults) {
|
||||
event.preventDefault();
|
||||
if (document.activeElement == input) {
|
||||
first.focus();
|
||||
} else if (document.activeElement == last) {
|
||||
last.focus();
|
||||
} else {
|
||||
document.activeElement.parentElement.nextSibling.firstElementChild.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Up arrow to move up results list
|
||||
if (event.key == "ArrowUp") {
|
||||
if (searchVisible && hasResults) {
|
||||
event.preventDefault();
|
||||
if (document.activeElement == input) {
|
||||
input.focus();
|
||||
} else if (document.activeElement == first) {
|
||||
input.focus();
|
||||
} else {
|
||||
document.activeElement.parentElement.previousSibling.firstElementChild.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enter to get to results
|
||||
if (event.key == "Enter") {
|
||||
if (searchVisible && hasResults) {
|
||||
event.preventDefault();
|
||||
if (document.activeElement == input) {
|
||||
first.focus();
|
||||
} else {
|
||||
document.activeElement.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update search on each keypress
|
||||
input.onkeyup = function (event) {
|
||||
executeQuery(this.value);
|
||||
};
|
||||
|
||||
function displaySearch() {
|
||||
if (!indexed) {
|
||||
buildIndex();
|
||||
}
|
||||
if (!searchVisible) {
|
||||
document.body.style.overflow = "hidden";
|
||||
wrapper.style.visibility = "visible";
|
||||
input.focus();
|
||||
searchVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
function hideSearch() {
|
||||
if (searchVisible) {
|
||||
document.body.style.overflow = "visible";
|
||||
wrapper.style.visibility = "hidden";
|
||||
input.value = "";
|
||||
output.innerHTML = "";
|
||||
document.activeElement.blur();
|
||||
searchVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchJSON(path, callback) {
|
||||
var httpRequest = new XMLHttpRequest();
|
||||
httpRequest.onreadystatechange = function () {
|
||||
if (httpRequest.readyState === 4) {
|
||||
if (httpRequest.status === 200) {
|
||||
var data = JSON.parse(httpRequest.responseText);
|
||||
if (callback) callback(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
httpRequest.open("GET", path);
|
||||
httpRequest.send();
|
||||
}
|
||||
|
||||
function buildIndex() {
|
||||
var baseURL = wrapper.getAttribute("data-url");
|
||||
baseURL = baseURL.replace(/\/?$/, "/");
|
||||
fetchJSON(baseURL + "index.json", function (data) {
|
||||
var options = {
|
||||
shouldSort: true,
|
||||
ignoreLocation: true,
|
||||
threshold: 0.0,
|
||||
includeMatches: true,
|
||||
keys: [
|
||||
{ name: "title", weight: 0.8 },
|
||||
{ name: "section", weight: 0.2 },
|
||||
{ name: "summary", weight: 0.6 },
|
||||
{ name: "content", weight: 0.4 },
|
||||
],
|
||||
};
|
||||
/*var finalIndex = [];
|
||||
for (var i in data) {
|
||||
if(data[i].type != "users" && data[i].type != "tags" && data[i].type != "categories"){
|
||||
finalIndex.push(data[i]);
|
||||
}
|
||||
}*/
|
||||
fuse = new Fuse(data, options);
|
||||
indexed = true;
|
||||
});
|
||||
}
|
||||
|
||||
function executeQuery(term) {
|
||||
let results = fuse.search(term);
|
||||
let resultsHTML = "";
|
||||
|
||||
if (results.length > 0) {
|
||||
results.forEach(function (value, key) {
|
||||
var html = value.item.summary;
|
||||
var div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
value.item.summary = div.textContent || div.innerText || "";
|
||||
var title = value.item.externalUrl
|
||||
? value.item.title +
|
||||
'<span class="text-xs ml-2 align-center cursor-default text-neutral-400 dark:text-neutral-500">' +
|
||||
value.item.externalUrl +
|
||||
"</span>"
|
||||
: value.item.title;
|
||||
var linkconfig = value.item.externalUrl
|
||||
? 'target="_blank" rel="noopener" href="' + value.item.externalUrl + '"'
|
||||
: 'href="' + value.item.permalink + '"';
|
||||
resultsHTML =
|
||||
resultsHTML +
|
||||
`<li class="mb-2">
|
||||
<a class="flex items-center px-3 py-2 rounded-md appearance-none bg-neutral-100 dark:bg-neutral-700 focus:bg-primary-100 hover:bg-primary-100 dark:hover:bg-primary-900 dark:focus:bg-primary-900 focus:outline-dotted focus:outline-transparent focus:outline-2"
|
||||
${linkconfig} tabindex="0">
|
||||
<div class="grow">
|
||||
<div class="-mb-1 text-lg font-bold">
|
||||
${title}
|
||||
</div>
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400">${value.item.section}<span class="px-2 text-primary-500">·</span>${value.item.date ? value.item.date : ""}</span></div>
|
||||
<div class="text-sm italic">${value.item.summary}</div>
|
||||
</div>
|
||||
<div class="ml-2 ltr:block rtl:hidden text-neutral-500">→</div>
|
||||
<div class="mr-2 ltr:hidden rtl:block text-neutral-500">←</div>
|
||||
</a>
|
||||
</li>`;
|
||||
});
|
||||
hasResults = true;
|
||||
} else {
|
||||
resultsHTML = "";
|
||||
hasResults = false;
|
||||
}
|
||||
|
||||
output.innerHTML = resultsHTML;
|
||||
if (results.length > 0) {
|
||||
first = output.firstChild.firstElementChild;
|
||||
last = output.lastChild.firstElementChild;
|
||||
}
|
||||
}
|
||||
39
assets/js/shortcodes/gallery.js
Normal file
39
assets/js/shortcodes/gallery.js
Normal file
@@ -0,0 +1,39 @@
|
||||
function _getDefaultPackeryOptions() {
|
||||
return {
|
||||
percentPosition: true,
|
||||
gutter: 5,
|
||||
resize: true,
|
||||
};
|
||||
}
|
||||
|
||||
function _getPackeryOptions(nodeGallery) {
|
||||
const defaults = _getDefaultPackeryOptions();
|
||||
const {
|
||||
packeryGutter,
|
||||
packeryPercentPosition,
|
||||
packeryResize,
|
||||
} = nodeGallery.dataset;
|
||||
|
||||
return {
|
||||
percentPosition:
|
||||
packeryPercentPosition !== undefined
|
||||
? packeryPercentPosition === "true"
|
||||
: defaults.percentPosition,
|
||||
gutter:
|
||||
packeryGutter !== undefined ? parseInt(packeryGutter, 10) : defaults.gutter,
|
||||
resize:
|
||||
packeryResize !== undefined ? packeryResize === "true" : defaults.resize,
|
||||
};
|
||||
}
|
||||
|
||||
(function init() {
|
||||
window.addEventListener("load", function () {
|
||||
let packeries = [];
|
||||
let nodeGalleries = document.querySelectorAll(".gallery");
|
||||
|
||||
nodeGalleries.forEach((nodeGallery) => {
|
||||
let packery = new Packery(nodeGallery, _getPackeryOptions(nodeGallery));
|
||||
packeries.push(packery);
|
||||
});
|
||||
});
|
||||
})();
|
||||
59
assets/js/shortcodes/tabs.js
Normal file
59
assets/js/shortcodes/tabs.js
Normal file
@@ -0,0 +1,59 @@
|
||||
function initTabs() {
|
||||
tabClickHandler = (event) => {
|
||||
const button = event.target.closest(".tab__button");
|
||||
if (!button) return;
|
||||
|
||||
const container = button.closest(".tab__container");
|
||||
const tabIndex = parseInt(button.dataset.tabIndex);
|
||||
const tabLabel = button.dataset.tabLabel;
|
||||
const group = container.dataset.tabGroup;
|
||||
|
||||
if (group) {
|
||||
const allGroupContainers = document.querySelectorAll(`.tab__container[data-tab-group="${group}"]`);
|
||||
|
||||
allGroupContainers.forEach((groupContainer) => {
|
||||
const targetButton = Array.from(groupContainer.querySelectorAll(".tab__button")).find(
|
||||
(btn) => btn.dataset.tabLabel === tabLabel,
|
||||
);
|
||||
|
||||
if (targetButton) {
|
||||
const targetIndex = parseInt(targetButton.dataset.tabIndex);
|
||||
activateTab(groupContainer, targetIndex);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
activateTab(container, tabIndex);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", tabClickHandler);
|
||||
}
|
||||
|
||||
function activateTab(container, activeIndex) {
|
||||
const buttons = container.querySelectorAll(".tab__button");
|
||||
const panels = container.querySelectorAll(".tab__panel");
|
||||
|
||||
buttons.forEach((btn, index) => {
|
||||
if (index === activeIndex) {
|
||||
btn.classList.add("tab--active");
|
||||
btn.setAttribute("aria-selected", "true");
|
||||
} else {
|
||||
btn.classList.remove("tab--active");
|
||||
btn.setAttribute("aria-selected", "false");
|
||||
}
|
||||
});
|
||||
|
||||
panels.forEach((panel, index) => {
|
||||
if (index === activeIndex) {
|
||||
panel.classList.add("tab--active");
|
||||
} else {
|
||||
panel.classList.remove("tab--active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initTabs);
|
||||
} else {
|
||||
initTabs();
|
||||
}
|
||||
67
assets/js/zen-mode.js
Normal file
67
assets/js/zen-mode.js
Normal file
@@ -0,0 +1,67 @@
|
||||
function _toggleZenMode(zendModeButton, options = { scrollToHeader: true }) {
|
||||
// Nodes selection
|
||||
const body = document.querySelector("body");
|
||||
const footer = document.querySelector("footer");
|
||||
const tocRight = document.querySelector(".toc-right");
|
||||
const tocInside = document.querySelector(".toc-inside");
|
||||
const articleContent = document.querySelector(".article-content");
|
||||
const header = document.querySelector("#single_header");
|
||||
|
||||
// Add semantic class into body tag
|
||||
body.classList.toggle("zen-mode-enable");
|
||||
|
||||
// Show/Hide 'toc right' and 'toc inside'
|
||||
if (tocRight) tocRight.classList.toggle("lg:block");
|
||||
if (tocInside) tocInside.classList.toggle("lg:hidden");
|
||||
|
||||
// Change width of article content
|
||||
articleContent.classList.toggle("max-w-fit");
|
||||
articleContent.classList.toggle("max-w-prose");
|
||||
|
||||
// Change width of article title and footer
|
||||
header.classList.toggle("max-w-full");
|
||||
header.classList.toggle("max-w-prose");
|
||||
footer.classList.toggle("max-w-full");
|
||||
footer.classList.toggle("max-w-prose");
|
||||
|
||||
// Read i18n title from data-attributes
|
||||
const titleI18nDisable = zendModeButton.getAttribute("data-title-i18n-disable");
|
||||
const titleI18nEnable = zendModeButton.getAttribute("data-title-i18n-enable");
|
||||
|
||||
if (body.classList.contains("zen-mode-enable")) {
|
||||
// Persist configuration
|
||||
//localStorage.setItem('blowfish-zen-mode-enabled', 'true');
|
||||
|
||||
// Change title to enable
|
||||
zendModeButton.setAttribute("title", titleI18nEnable);
|
||||
// Auto-scroll to title article
|
||||
if (options.scrollToHeader) {
|
||||
window.scrollTo(window.scrollX, header.getBoundingClientRect().top - 90);
|
||||
}
|
||||
} else {
|
||||
//localStorage.setItem('blowfish-zen-mode-enabled', 'false');
|
||||
zendModeButton.setAttribute("title", titleI18nDisable);
|
||||
if (options.scrollToHeader) {
|
||||
document.querySelector("body").scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _registerZendModeButtonClick(zendModeButton) {
|
||||
zendModeButton.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Toggle zen-mode
|
||||
_toggleZenMode(zendModeButton);
|
||||
});
|
||||
}
|
||||
|
||||
(function init() {
|
||||
window.addEventListener("DOMContentLoaded", (event) => {
|
||||
// Register click on 'zen-mode-button' node element
|
||||
const zendModeButton = document.getElementById("zen-mode-button");
|
||||
if (zendModeButton !== null && zendModeButton !== undefined) {
|
||||
_registerZendModeButtonClick(zendModeButton);
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user