feat(infra): migración a tema Apollo, reestructuración del blog y página 'Sobre mí'
All checks were successful
Zola / build-and-deploy (push) Successful in 11s
All checks were successful
Zola / build-and-deploy (push) Successful in 11s
- Implementado tema Apollo como base visual. - Segregación de contenido: Artículos movidos a /blog. - Creación de identidad: Nueva página 'Sobre mí' (about.md). - CI/CD: Actualizado workflow para soporte de submódulos recursivos. - UX: Ajustes en homepage para listado de últimos posts.
This commit is contained in:
parent
0bbe34e8da
commit
4fb49961b4
115 changed files with 6580 additions and 72 deletions
103
public/js/codeblock.js
Normal file
103
public/js/codeblock.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
const successIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="bi bi-check-lg" viewBox="0 0 16 16">
|
||||
<path d="M13.485 1.85a.5.5 0 0 1 1.065.02.75.75 0 0 1-.02 1.065L5.82 12.78a.75.75 0 0 1-1.106.02L1.476 9.346a.75.75 0 1 1 1.05-1.07l2.74 2.742L12.44 2.92a.75.75 0 0 1 1.045-.07z"/>
|
||||
</svg>`;
|
||||
const errorIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="bi bi-x-lg" viewBox="0 0 16 16">
|
||||
<path d="M2.293 2.293a1 1 0 0 1 1.414 0L8 6.586l4.293-4.293a1 1 0 0 1 1.414 1.414L9.414 8l4.293 4.293a1 1 0 0 1-1.414 1.414L8 9.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L6.586 8 2.293 3.707a1 1 0 0 1 0-1.414z"/>
|
||||
</svg>`;
|
||||
const copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="bi bi-clipboard" viewBox="0 0 16 16">
|
||||
<path d="M10 1.5a.5.5 0 0 1 .5-.5h2a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-9a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h2a.5.5 0 0 1 .5.5V3h3V1.5zM6.5 3V2h3v1h-3zm4 0v1h2a1 1 0 0 0-1-1h-2V3zm-5 0H3a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H5.5V3z"/>
|
||||
</svg>`;
|
||||
|
||||
// Function to change icons after copying
|
||||
const changeIcon = (button, isSuccess) => {
|
||||
button.innerHTML = isSuccess ? successIcon : errorIcon;
|
||||
setTimeout(() => {
|
||||
button.innerHTML = copyIcon; // Reset to copy icon
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// Function to get code text from tables, skipping line numbers
|
||||
const getCodeFromTable = (codeBlock) => {
|
||||
return [...codeBlock.querySelectorAll('tr')]
|
||||
.map(row => row.querySelector('td:last-child')?.innerText ?? '')
|
||||
.join('');
|
||||
};
|
||||
|
||||
// Function to get code text from non-table blocks
|
||||
const getNonTableCode = (codeBlock) => {
|
||||
return codeBlock.textContent.trim();
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Select all `pre` elements containing `code`
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
const pre = entry.target.parentNode;
|
||||
const clipboardBtn = pre.querySelector('.clipboard-button');
|
||||
const label = pre.querySelector('.code-label');
|
||||
|
||||
if (clipboardBtn) {
|
||||
// Adjust the position of the clipboard button when the `code` is not fully visible
|
||||
clipboardBtn.style.right = entry.isIntersecting ? '5px' : `-${entry.boundingClientRect.right - pre.clientWidth + 5}px`;
|
||||
}
|
||||
|
||||
if (label) {
|
||||
// Adjust the position of the label similarly
|
||||
label.style.right = entry.isIntersecting ? '0px' : `-${entry.boundingClientRect.right - pre.clientWidth}px`;
|
||||
}
|
||||
});
|
||||
}, {
|
||||
root: null, // observing relative to viewport
|
||||
rootMargin: '0px',
|
||||
threshold: 1.0 // Adjust this to control when the callback fires
|
||||
});
|
||||
|
||||
document.querySelectorAll('pre code').forEach(codeBlock => {
|
||||
const pre = codeBlock.parentNode;
|
||||
pre.style.position = 'relative'; // Ensure parent `pre` can contain absolute elements
|
||||
|
||||
// Create and append the copy button
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'clipboard-button';
|
||||
copyBtn.innerHTML = copyIcon;
|
||||
copyBtn.setAttribute('aria-label', 'Copy code to clipboard');
|
||||
pre.appendChild(copyBtn);
|
||||
|
||||
// Attach event listener to copy button
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
// Determine if the code is in a table or not
|
||||
const isTable = codeBlock.querySelector('table');
|
||||
const codeToCopy = isTable ? getCodeFromTable(codeBlock) : getNonTableCode(codeBlock);
|
||||
try {
|
||||
await navigator.clipboard.writeText(codeToCopy);
|
||||
changeIcon(copyBtn, true); // Show success icon
|
||||
} catch (error) {
|
||||
console.error('Failed to copy text: ', error);
|
||||
changeIcon(copyBtn, false); // Show error icon
|
||||
}
|
||||
});
|
||||
|
||||
const langClass = codeBlock.className.match(/language-(\w+)/);
|
||||
const lang = langClass ? langClass[1] : 'default';
|
||||
|
||||
// Create and append the label
|
||||
const label = document.createElement('span');
|
||||
label.className = 'code-label label-' + lang; // Use the specific language class
|
||||
label.textContent = lang.toUpperCase(); // Display the language as label
|
||||
pre.appendChild(label);
|
||||
|
||||
let ticking = false;
|
||||
pre.addEventListener('scroll', () => {
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
copyBtn.style.right = `-${pre.scrollLeft}px`;
|
||||
label.style.right = `-${pre.scrollLeft}px`;
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
271
public/js/count.js
Normal file
271
public/js/count.js
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
// GoatCounter: https://www.goatcounter.com
|
||||
// This file (and *only* this file) is released under the ISC license: https://opensource.org/licenses/ISC
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
if (window.goatcounter && window.goatcounter.vars) // Compatibility with very old version; do not use.
|
||||
window.goatcounter = window.goatcounter.vars
|
||||
else
|
||||
window.goatcounter = window.goatcounter || {}
|
||||
|
||||
// Load settings from data-goatcounter-settings.
|
||||
var s = document.querySelector('script[data-goatcounter]')
|
||||
if (s && s.dataset.goatcounterSettings) {
|
||||
try { var set = JSON.parse(s.dataset.goatcounterSettings) }
|
||||
catch (err) { console.error('invalid JSON in data-goatcounter-settings: ' + err) }
|
||||
for (var k in set)
|
||||
if (['no_onload', 'no_events', 'allow_local', 'allow_frame', 'path', 'title', 'referrer', 'event'].indexOf(k) > -1)
|
||||
window.goatcounter[k] = set[k]
|
||||
}
|
||||
|
||||
var enc = encodeURIComponent
|
||||
|
||||
// Get all data we're going to send off to the counter endpoint.
|
||||
var get_data = function(vars) {
|
||||
var data = {
|
||||
p: (vars.path === undefined ? goatcounter.path : vars.path),
|
||||
r: (vars.referrer === undefined ? goatcounter.referrer : vars.referrer),
|
||||
t: (vars.title === undefined ? goatcounter.title : vars.title),
|
||||
e: !!(vars.event || goatcounter.event),
|
||||
s: [window.screen.width, window.screen.height, (window.devicePixelRatio || 1)],
|
||||
b: is_bot(),
|
||||
q: location.search,
|
||||
}
|
||||
|
||||
var rcb, pcb, tcb // Save callbacks to apply later.
|
||||
if (typeof(data.r) === 'function') rcb = data.r
|
||||
if (typeof(data.t) === 'function') tcb = data.t
|
||||
if (typeof(data.p) === 'function') pcb = data.p
|
||||
|
||||
if (is_empty(data.r)) data.r = document.referrer
|
||||
if (is_empty(data.t)) data.t = document.title
|
||||
if (is_empty(data.p)) data.p = get_path()
|
||||
|
||||
if (rcb) data.r = rcb(data.r)
|
||||
if (tcb) data.t = tcb(data.t)
|
||||
if (pcb) data.p = pcb(data.p)
|
||||
return data
|
||||
}
|
||||
|
||||
// Check if a value is "empty" for the purpose of get_data().
|
||||
var is_empty = function(v) { return v === null || v === undefined || typeof(v) === 'function' }
|
||||
|
||||
// See if this looks like a bot; there is some additional filtering on the
|
||||
// backend, but these properties can't be fetched from there.
|
||||
var is_bot = function() {
|
||||
// Headless browsers are probably a bot.
|
||||
var w = window, d = document
|
||||
if (w.callPhantom || w._phantom || w.phantom)
|
||||
return 150
|
||||
if (w.__nightmare)
|
||||
return 151
|
||||
if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate)
|
||||
return 152
|
||||
if (navigator.webdriver)
|
||||
return 153
|
||||
return 0
|
||||
}
|
||||
|
||||
// Object to urlencoded string, starting with a ?.
|
||||
var urlencode = function(obj) {
|
||||
var p = []
|
||||
for (var k in obj)
|
||||
if (obj[k] !== '' && obj[k] !== null && obj[k] !== undefined && obj[k] !== false)
|
||||
p.push(enc(k) + '=' + enc(obj[k]))
|
||||
return '?' + p.join('&')
|
||||
}
|
||||
|
||||
// Show a warning in the console.
|
||||
var warn = function(msg) {
|
||||
if (console && 'warn' in console)
|
||||
console.warn('goatcounter: ' + msg)
|
||||
}
|
||||
|
||||
// Get the endpoint to send requests to.
|
||||
var get_endpoint = function() {
|
||||
var s = document.querySelector('script[data-goatcounter]')
|
||||
if (s && s.dataset.goatcounter)
|
||||
return s.dataset.goatcounter
|
||||
return (goatcounter.endpoint || window.counter) // counter is for compat; don't use.
|
||||
}
|
||||
|
||||
// Get current path.
|
||||
var get_path = function() {
|
||||
var loc = location,
|
||||
c = document.querySelector('link[rel="canonical"][href]')
|
||||
if (c) { // May be relative or point to different domain.
|
||||
var a = document.createElement('a')
|
||||
a.href = c.href
|
||||
if (a.hostname.replace(/^www\./, '') === location.hostname.replace(/^www\./, ''))
|
||||
loc = a
|
||||
}
|
||||
return (loc.pathname + loc.search) || '/'
|
||||
}
|
||||
|
||||
// Run function after DOM is loaded.
|
||||
var on_load = function(f) {
|
||||
if (document.body === null)
|
||||
document.addEventListener('DOMContentLoaded', function() { f() }, false)
|
||||
else
|
||||
f()
|
||||
}
|
||||
|
||||
// Filter some requests that we (probably) don't want to count.
|
||||
goatcounter.filter = function() {
|
||||
if ('visibilityState' in document && document.visibilityState === 'prerender')
|
||||
return 'visibilityState'
|
||||
if (!goatcounter.allow_frame && location !== parent.location)
|
||||
return 'frame'
|
||||
if (!goatcounter.allow_local && location.hostname.match(/(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^0\.0\.0\.0$)/))
|
||||
return 'localhost'
|
||||
if (!goatcounter.allow_local && location.protocol === 'file:')
|
||||
return 'localfile'
|
||||
if (localStorage && localStorage.getItem('skipgc') === 't')
|
||||
return 'disabled with #toggle-goatcounter'
|
||||
return false
|
||||
}
|
||||
|
||||
// Get URL to send to GoatCounter.
|
||||
window.goatcounter.url = function(vars) {
|
||||
var data = get_data(vars || {})
|
||||
if (data.p === null) // null from user callback.
|
||||
return
|
||||
data.rnd = Math.random().toString(36).substr(2, 5) // Browsers don't always listen to Cache-Control.
|
||||
|
||||
var endpoint = get_endpoint()
|
||||
if (!endpoint)
|
||||
return warn('no endpoint found')
|
||||
|
||||
return endpoint + urlencode(data)
|
||||
}
|
||||
|
||||
// Count a hit.
|
||||
window.goatcounter.count = function(vars) {
|
||||
var f = goatcounter.filter()
|
||||
if (f)
|
||||
return warn('not counting because of: ' + f)
|
||||
var url = goatcounter.url(vars)
|
||||
if (!url)
|
||||
return warn('not counting because path callback returned null')
|
||||
|
||||
if (!navigator.sendBeacon(url)) {
|
||||
// This mostly fails due to being blocked by CSP; try again with an
|
||||
// image-based fallback.
|
||||
var img = document.createElement('img')
|
||||
img.src = url
|
||||
img.style.position = 'absolute' // Affect layout less.
|
||||
img.style.bottom = '0px'
|
||||
img.style.width = '1px'
|
||||
img.style.height = '1px'
|
||||
img.loading = 'eager'
|
||||
img.setAttribute('alt', '')
|
||||
img.setAttribute('aria-hidden', 'true')
|
||||
|
||||
var rm = function() { if (img && img.parentNode) img.parentNode.removeChild(img) }
|
||||
img.addEventListener('load', rm, false)
|
||||
document.body.appendChild(img)
|
||||
}
|
||||
}
|
||||
|
||||
// Get a query parameter.
|
||||
window.goatcounter.get_query = function(name) {
|
||||
var s = location.search.substr(1).split('&')
|
||||
for (var i = 0; i < s.length; i++)
|
||||
if (s[i].toLowerCase().indexOf(name.toLowerCase() + '=') === 0)
|
||||
return s[i].substr(name.length + 1)
|
||||
}
|
||||
|
||||
// Track click events.
|
||||
window.goatcounter.bind_events = function() {
|
||||
if (!document.querySelectorAll) // Just in case someone uses an ancient browser.
|
||||
return
|
||||
|
||||
var send = function(elem) {
|
||||
return function() {
|
||||
goatcounter.count({
|
||||
event: true,
|
||||
path: (elem.dataset.goatcounterClick || elem.name || elem.id || ''),
|
||||
title: (elem.dataset.goatcounterTitle || elem.title || (elem.innerHTML || '').substr(0, 200) || ''),
|
||||
referrer: (elem.dataset.goatcounterReferrer || elem.dataset.goatcounterReferral || ''),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Array.prototype.slice.call(document.querySelectorAll("*[data-goatcounter-click]")).forEach(function(elem) {
|
||||
if (elem.dataset.goatcounterBound)
|
||||
return
|
||||
var f = send(elem)
|
||||
elem.addEventListener('click', f, false)
|
||||
elem.addEventListener('auxclick', f, false) // Middle click.
|
||||
elem.dataset.goatcounterBound = 'true'
|
||||
})
|
||||
}
|
||||
|
||||
// Add a "visitor counter" frame or image.
|
||||
window.goatcounter.visit_count = function(opt) {
|
||||
on_load(function() {
|
||||
opt = opt || {}
|
||||
opt.type = opt.type || 'html'
|
||||
opt.append = opt.append || 'body'
|
||||
opt.path = opt.path || get_path()
|
||||
opt.attr = opt.attr || {width: '200', height: (opt.no_branding ? '60' : '80')}
|
||||
|
||||
opt.attr['src'] = get_endpoint() + 'er/' + enc(opt.path) + '.' + enc(opt.type) + '?'
|
||||
if (opt.no_branding) opt.attr['src'] += '&no_branding=1'
|
||||
if (opt.style) opt.attr['src'] += '&style=' + enc(opt.style)
|
||||
if (opt.start) opt.attr['src'] += '&start=' + enc(opt.start)
|
||||
if (opt.end) opt.attr['src'] += '&end=' + enc(opt.end)
|
||||
|
||||
var tag = {png: 'img', svg: 'img', html: 'iframe'}[opt.type]
|
||||
if (!tag)
|
||||
return warn('visit_count: unknown type: ' + opt.type)
|
||||
|
||||
if (opt.type === 'html') {
|
||||
opt.attr['frameborder'] = '0'
|
||||
opt.attr['scrolling'] = 'no'
|
||||
}
|
||||
|
||||
var d = document.createElement(tag)
|
||||
for (var k in opt.attr)
|
||||
d.setAttribute(k, opt.attr[k])
|
||||
|
||||
var p = document.querySelector(opt.append)
|
||||
if (!p)
|
||||
return warn('visit_count: append not found: ' + opt.append)
|
||||
p.appendChild(d)
|
||||
})
|
||||
}
|
||||
|
||||
// Make it easy to skip your own views.
|
||||
if (location.hash === '#toggle-goatcounter') {
|
||||
if (localStorage.getItem('skipgc') === 't') {
|
||||
localStorage.removeItem('skipgc', 't')
|
||||
alert('GoatCounter tracking is now ENABLED in this browser.')
|
||||
}
|
||||
else {
|
||||
localStorage.setItem('skipgc', 't')
|
||||
alert('GoatCounter tracking is now DISABLED in this browser until ' + location + ' is loaded again.')
|
||||
}
|
||||
}
|
||||
|
||||
if (!goatcounter.no_onload)
|
||||
on_load(function() {
|
||||
// 1. Page is visible, count request.
|
||||
// 2. Page is not yet visible; wait until it switches to 'visible' and count.
|
||||
// See #487
|
||||
if (!('visibilityState' in document) || document.visibilityState === 'visible')
|
||||
goatcounter.count()
|
||||
else {
|
||||
var f = function(e) {
|
||||
if (document.visibilityState !== 'visible')
|
||||
return
|
||||
document.removeEventListener('visibilitychange', f)
|
||||
goatcounter.count()
|
||||
}
|
||||
document.addEventListener('visibilitychange', f)
|
||||
}
|
||||
|
||||
if (!goatcounter.no_events)
|
||||
goatcounter.bind_events()
|
||||
})
|
||||
})();
|
||||
2
public/js/imamu.js
Normal file
2
public/js/imamu.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// https://cloud.umami.is/script.js
|
||||
!function(){"use strict";(t=>{const{screen:{width:e,height:a},navigator:{language:r},location:n,document:i,history:c}=t,{hostname:s,href:o,origin:u}=n,{currentScript:l,referrer:d}=i,h=o.startsWith("data:")?void 0:t.localStorage;if(!l)return;const m="data-",f=l.getAttribute.bind(l),p=f(m+"website-id"),g=f(m+"host-url"),y=f(m+"tag"),b="false"!==f(m+"auto-track"),v="true"===f(m+"exclude-search"),w=f(m+"domains")||"",S=w.split(",").map((t=>t.trim())),N=`${(g||"https://api-gateway.umami.dev"||l.src.split("/").slice(0,-1).join("/")).replace(/\/$/,"")}/api/send`,T=`${e}x${a}`,A=/data-umami-event-([\w-_]+)/,x=m+"umami-event",O=300,U=t=>{if(t){try{const e=decodeURI(t);if(e!==t)return e}catch(e){return t}return encodeURI(t)}},j=t=>{try{const{pathname:e,search:a,hash:r}=new URL(t,n.href);t=e+a+r}catch(t){}return v?t.split("?")[0]:t},k=()=>({website:p,hostname:s,screen:T,language:r,title:U(q),url:U(W),referrer:U(_),tag:y||void 0}),E=(t,e,a)=>{a&&(_=W,W=j(a.toString()),W!==_&&setTimeout(K,O))},L=()=>!p||h&&h.getItem("umami.disabled")||w&&!S.includes(s),$=async(t,e="event")=>{if(L())return;const a={"Content-Type":"application/json"};void 0!==B&&(a["x-umami-cache"]=B);try{const r=await fetch(N,{method:"POST",body:JSON.stringify({type:e,payload:t}),headers:a}),n=await r.text();return B=n}catch(t){}},I=()=>{D||(K(),(()=>{const t=(t,e,a)=>{const r=t[e];return(...e)=>(a.apply(null,e),r.apply(t,e))};c.pushState=t(c,"pushState",E),c.replaceState=t(c,"replaceState",E)})(),(()=>{const t=new MutationObserver((([t])=>{q=t&&t.target?t.target.text:void 0})),e=i.querySelector("head > title");e&&t.observe(e,{subtree:!0,characterData:!0,childList:!0})})(),i.addEventListener("click",(async t=>{const e=t=>["BUTTON","A"].includes(t),a=async t=>{const e=t.getAttribute.bind(t),a=e(x);if(a){const r={};return t.getAttributeNames().forEach((t=>{const a=t.match(A);a&&(r[a[1]]=e(t))})),K(a,r)}},r=t.target,i=e(r.tagName)?r:((t,a)=>{let r=t;for(let t=0;t<a;t++){if(e(r.tagName))return r;if(r=r.parentElement,!r)return null}})(r,10);if(!i)return a(r);{const{href:e,target:r}=i,c=i.getAttribute(x);if(c)if("A"===i.tagName){const s="_blank"===r||t.ctrlKey||t.shiftKey||t.metaKey||t.button&&1===t.button;if(c&&e)return s||t.preventDefault(),a(i).then((()=>{s||(n.href=e)}))}else if("BUTTON"===i.tagName)return a(i)}}),!0),D=!0)},K=(t,e)=>$("string"==typeof t?{...k(),name:t,data:"object"==typeof e?e:void 0}:"object"==typeof t?t:"function"==typeof t?t(k()):k()),R=t=>$({...k(),data:t},"identify");t.umami||(t.umami={track:K,identify:R});let B,D,W=j(o),_=d.startsWith(u)?"":d,q=i.title;b&&!L()&&("complete"===i.readyState?I():i.addEventListener("readystatechange",I,!0))})(window)}();
|
||||
25
public/js/main.js
Normal file
25
public/js/main.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
mmdElements = document.getElementsByClassName("mermaid");
|
||||
const mmdHTML = [];
|
||||
for (let i = 0; i < mmdElements.length; i++) {
|
||||
mmdHTML[i] = mmdElements[i].innerHTML;
|
||||
}
|
||||
|
||||
function mermaidRender(theme) {
|
||||
if (theme == "dark") {
|
||||
initOptions = {
|
||||
startOnLoad: false,
|
||||
theme: "dark",
|
||||
};
|
||||
} else {
|
||||
initOptions = {
|
||||
startOnLoad: false,
|
||||
theme: "neutral",
|
||||
};
|
||||
}
|
||||
for (let i = 0; i < mmdElements.length; i++) {
|
||||
delete mmdElements[i].dataset.processed;
|
||||
mmdElements[i].innerHTML = mmdHTML[i];
|
||||
}
|
||||
mermaid.initialize(initOptions);
|
||||
mermaid.run();
|
||||
}
|
||||
1760
public/js/mermaid.js
Normal file
1760
public/js/mermaid.js
Normal file
File diff suppressed because one or more lines are too long
14
public/js/note.js
Normal file
14
public/js/note.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.note-toggle').forEach(function(toggleButton) {
|
||||
var content = toggleButton.nextElementSibling;
|
||||
var isHidden = content.style.display === 'none';
|
||||
toggleButton.setAttribute('aria-expanded', !isHidden);
|
||||
|
||||
toggleButton.addEventListener('click', function() {
|
||||
var expanded = this.getAttribute('aria-expanded') === 'true';
|
||||
this.setAttribute('aria-expanded', !expanded);
|
||||
content.style.display = expanded ? 'none' : 'block';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
3201
public/js/searchElasticlunr.js
Normal file
3201
public/js/searchElasticlunr.js
Normal file
File diff suppressed because it is too large
Load diff
1
public/js/searchElasticlunr.min.js
vendored
Normal file
1
public/js/searchElasticlunr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
82
public/js/themetoggle.js
Normal file
82
public/js/themetoggle.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
function setTheme(mode) {
|
||||
localStorage.setItem("theme-storage", mode);
|
||||
}
|
||||
|
||||
// Functions needed for the theme toggle
|
||||
//
|
||||
|
||||
function toggleTheme() {
|
||||
const currentTheme = getSavedTheme();
|
||||
if (currentTheme === "light") {
|
||||
setTheme("dark");
|
||||
updateItemToggleTheme();
|
||||
} else if (currentTheme === "dark") {
|
||||
setTheme("auto");
|
||||
updateItemToggleTheme();
|
||||
} else {
|
||||
setTheme("light");
|
||||
updateItemToggleTheme();
|
||||
}
|
||||
}
|
||||
|
||||
function updateItemToggleTheme() {
|
||||
let mode = getSavedTheme();
|
||||
|
||||
const darkModeStyle = document.getElementById("darkModeStyle");
|
||||
if (darkModeStyle) {
|
||||
if (mode === "dark" || (mode === "auto" && getSystemPrefersDark())) {
|
||||
darkModeStyle.disabled = false;
|
||||
} else {
|
||||
darkModeStyle.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
const sunIcon = document.getElementById("sun-icon");
|
||||
const moonIcon = document.getElementById("moon-icon");
|
||||
const autoIcon = document.getElementById("auto-icon");
|
||||
if (sunIcon && moonIcon && autoIcon) {
|
||||
sunIcon.style.display = (mode === "light") ? "block" : "none";
|
||||
moonIcon.style.display = (mode === "dark") ? "block" : "none";
|
||||
autoIcon.style.display = (mode === "auto") ? "block" : "none";
|
||||
|
||||
if (mode === "auto") {
|
||||
autoIcon.style.filter = getSystemPrefersDark() ? "invert(1)" : "invert(0)";
|
||||
} else {
|
||||
autoIcon.style.filter = "none";
|
||||
}
|
||||
}
|
||||
|
||||
let htmlElement = document.querySelector("html");
|
||||
if (mode === "dark" || (mode === "auto" && getSystemPrefersDark())) {
|
||||
htmlElement.classList.remove("light")
|
||||
htmlElement.classList.add("dark")
|
||||
} else {
|
||||
htmlElement.classList.remove("dark")
|
||||
htmlElement.classList.add("light")
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemPrefersDark() {
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
function getSavedTheme() {
|
||||
let currentTheme = localStorage.getItem("theme-storage");
|
||||
if(!currentTheme) {
|
||||
currentTheme = getSystemPrefersDark() ? "dark" : "light";
|
||||
}
|
||||
|
||||
return currentTheme;
|
||||
}
|
||||
|
||||
// Update the toggle theme on page load
|
||||
updateItemToggleTheme();
|
||||
|
||||
// Listen for system theme changes in auto mode
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
||||
if (getSavedTheme() === "auto") {
|
||||
updateItemToggleTheme();
|
||||
}
|
||||
});
|
||||
}
|
||||
49
public/js/toc.js
Normal file
49
public/js/toc.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
document.addEventListener("DOMContentLoaded", () => {
|
||||
let observer = new IntersectionObserver(handler, {
|
||||
threshold: [0],
|
||||
});
|
||||
let paragraphs = [...document.querySelectorAll("section > *")];
|
||||
let submenu = [...document.querySelectorAll(".toc a")];
|
||||
|
||||
function previousHeaderId(e) {
|
||||
for (; e && !e.matches("h1, h2, h3, h4"); ) e = e.previousElementSibling;
|
||||
return e?.id;
|
||||
}
|
||||
let paragraphMenuMap = paragraphs.reduce((e, t) => {
|
||||
let n = previousHeaderId(t);
|
||||
if (((t.previousHeader = n), n)) {
|
||||
let t = submenu.find((e) => decodeURIComponent(e.hash) === "#" + n);
|
||||
e[n] = t;
|
||||
}
|
||||
return e;
|
||||
}, {});
|
||||
|
||||
paragraphs.forEach((e) => observer.observe(e));
|
||||
let selection;
|
||||
function handler(e) {
|
||||
selection = (selection || e).map(
|
||||
(t) => e.find((e) => e.target === t.target) || t,
|
||||
);
|
||||
for (s of selection)
|
||||
s.isIntersecting ||
|
||||
paragraphMenuMap[
|
||||
s.target.previousHeader
|
||||
]?.parentElement.classList.remove("selected", "parent");
|
||||
for (s of selection)
|
||||
if (s.isIntersecting) {
|
||||
let e = paragraphMenuMap[s.target.previousHeader]?.closest("li");
|
||||
if ((e?.classList.add("selected"), e === void 0)) continue;
|
||||
// Find the anchor element within the list item
|
||||
let t = e.querySelector("a");
|
||||
if (t) {
|
||||
t.scrollIntoView({
|
||||
block: "nearest",
|
||||
inline: "nearest",
|
||||
});
|
||||
}
|
||||
for (; e; ) {
|
||||
e?.classList.add("parent"), (e = e.parentElement.closest("li"));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue