mirror of
https://github.com/andwati/andwati.com.git
synced 2026-11-10 19:05:12 +00:00
switched theme
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var body = document.getElementById('term-body');
|
||||
if (!body) return;
|
||||
|
||||
var path = window.location.pathname;
|
||||
var promptUser = body.dataset.prompt || 'user@pwnbox';
|
||||
var hostname = body.dataset.hostname || 'pwnbox';
|
||||
var whoami = body.dataset.whoami || 'l0st_r3ader';
|
||||
|
||||
var links = [
|
||||
{ href: body.dataset.linkHome, label: '~/home' },
|
||||
{ href: body.dataset.linkPosts, label: '~/posts' },
|
||||
{ href: body.dataset.linkTags, label: '~/tags' },
|
||||
];
|
||||
|
||||
var steps = [
|
||||
{ type: 'cmd', text: 'cd ' + path },
|
||||
{ type: 'out-error', text: 'bash: cd: ' + path + ': No such file or directory' },
|
||||
{ type: 'cmd', text: 'whoami' },
|
||||
{ type: 'out', text: whoami },
|
||||
{ type: 'cmd', text: 'ls ~/recovery/' },
|
||||
{ type: 'ls' },
|
||||
];
|
||||
|
||||
var CHAR_DELAY = 28;
|
||||
var LINE_DELAY = 180;
|
||||
|
||||
function ps1() {
|
||||
var s = document.createElement('span');
|
||||
s.className = 'term-ps1';
|
||||
s.textContent = promptUser + '@' + hostname + ':~$ ';
|
||||
return s;
|
||||
}
|
||||
|
||||
function appendCmdLine(text, callback) {
|
||||
var row = document.createElement('div');
|
||||
row.className = 'term-row';
|
||||
row.appendChild(ps1());
|
||||
var input = document.createElement('span');
|
||||
input.className = 'term-input';
|
||||
row.appendChild(input);
|
||||
body.appendChild(row);
|
||||
|
||||
var i = 0;
|
||||
function tick() {
|
||||
if (i < text.length) {
|
||||
input.textContent += text[i++];
|
||||
setTimeout(tick, CHAR_DELAY);
|
||||
} else {
|
||||
setTimeout(callback, LINE_DELAY);
|
||||
}
|
||||
}
|
||||
tick();
|
||||
}
|
||||
|
||||
function appendOut(text, extraClass, callback) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'term-out' + (extraClass ? ' ' + extraClass : '');
|
||||
div.textContent = text;
|
||||
body.appendChild(div);
|
||||
setTimeout(callback, LINE_DELAY);
|
||||
}
|
||||
|
||||
function appendLs(callback) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'term-out-ls';
|
||||
links.forEach(function (l) {
|
||||
var a = document.createElement('a');
|
||||
a.href = l.href;
|
||||
a.textContent = l.label;
|
||||
div.appendChild(a);
|
||||
});
|
||||
body.appendChild(div);
|
||||
setTimeout(callback, LINE_DELAY);
|
||||
}
|
||||
|
||||
function appendCursor() {
|
||||
var row = document.createElement('div');
|
||||
row.className = 'term-row';
|
||||
row.appendChild(ps1());
|
||||
var cur = document.createElement('span');
|
||||
cur.className = 'term-cursor';
|
||||
cur.setAttribute('aria-hidden', 'true');
|
||||
row.appendChild(cur);
|
||||
body.appendChild(row);
|
||||
}
|
||||
|
||||
function runSteps(i) {
|
||||
if (i >= steps.length) {
|
||||
appendCursor();
|
||||
return;
|
||||
}
|
||||
var step = steps[i];
|
||||
var next = function () {
|
||||
runSteps(i + 1);
|
||||
};
|
||||
|
||||
if (step.type === 'cmd') appendCmdLine(step.text, next);
|
||||
else if (step.type === 'out-error') appendOut(step.text, 'term-out-error', next);
|
||||
else if (step.type === 'out') appendOut(step.text, 'term-out-muted', next);
|
||||
else if (step.type === 'ls') appendLs(next);
|
||||
else next();
|
||||
}
|
||||
|
||||
runSteps(0);
|
||||
})();
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={757:function(t){t.exports=e}},n={};function r(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={exports:{}};return t[e](i,i.exports,r),i.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var o={};r.d(o,{default:function(){return p}});var i=r(757),a=r.n(i);const l=function(e,t,n){let r=n,o=0;const i=e.length;for(;r<t.length;){const n=t[r];if(o<=0&&t.slice(r,r+i)===e)return r;"\\"===n?r++:"{"===n?o++:"}"===n&&o--,r++}return-1},s=/^\\begin{/;var d=function(e,t){let n;const r=[],o=new RegExp("("+t.map((e=>e.left.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"))).join("|")+")");for(;n=e.search(o),-1!==n;){n>0&&(r.push({type:"text",data:e.slice(0,n)}),e=e.slice(n));const o=t.findIndex((t=>e.startsWith(t.left)));if(n=l(t[o].right,e,t[o].left.length),-1===n)break;const i=e.slice(0,n+t[o].right.length),a=s.test(i)?i:e.slice(t[o].left.length,n);r.push({type:"math",data:a,rawData:i,display:t[o].display}),e=e.slice(n+t[o].right.length)}return""!==e&&r.push({type:"text",data:e}),r};const c=function(e,t){const n=d(e,t.delimiters);if(1===n.length&&"text"===n[0].type)return null;const r=document.createDocumentFragment();for(let e=0;e<n.length;e++)if("text"===n[e].type)r.appendChild(document.createTextNode(n[e].data));else{const o=document.createElement("span");let i=n[e].data;t.displayMode=n[e].display;try{t.preProcess&&(i=t.preProcess(i)),a().render(i,o,t)}catch(o){if(!(o instanceof a().ParseError))throw o;t.errorCallback("KaTeX auto-render: Failed to parse `"+n[e].data+"` with ",o),r.appendChild(document.createTextNode(n[e].rawData));continue}r.appendChild(o)}return r},f=function(e,t){for(let n=0;n<e.childNodes.length;n++){const r=e.childNodes[n];if(3===r.nodeType){let o=r.textContent,i=r.nextSibling,a=0;for(;i&&i.nodeType===Node.TEXT_NODE;)o+=i.textContent,i=i.nextSibling,a++;const l=c(o,t);if(l){for(let e=0;e<a;e++)r.nextSibling.remove();n+=l.childNodes.length-1,e.replaceChild(l,r)}else n+=a}else if(1===r.nodeType){const e=" "+r.className+" ";-1===t.ignoredTags.indexOf(r.nodeName.toLowerCase())&&t.ignoredClasses.every((t=>-1===e.indexOf(" "+t+" ")))&&f(r,t)}}};var p=function(e,t){if(!e)throw new Error("No element provided to render");const n={};for(const e in t)t.hasOwnProperty(e)&&(n[e]=t[e]);n.delimiters=n.delimiters||[{left:"$$",right:"$$",display:!0},{left:"\\(",right:"\\)",display:!1},{left:"\\begin{equation}",right:"\\end{equation}",display:!0},{left:"\\begin{align}",right:"\\end{align}",display:!0},{left:"\\begin{alignat}",right:"\\end{alignat}",display:!0},{left:"\\begin{gather}",right:"\\end{gather}",display:!0},{left:"\\begin{CD}",right:"\\end{CD}",display:!0},{left:"\\[",right:"\\]",display:!0}],n.ignoredTags=n.ignoredTags||["script","noscript","style","textarea","pre","code","option"],n.ignoredClasses=n.ignoredClasses||[],n.errorCallback=n.errorCallback||console.error,n.macros=n.macros||{},f(e,n)};return o=o.default}()}));
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Select all <pre> elements that contain code blocks
|
||||
const preBlocks = document.querySelectorAll('pre');
|
||||
|
||||
const copyIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>';
|
||||
const checkIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>';
|
||||
|
||||
preBlocks.forEach((pre) => {
|
||||
// Create the copy button
|
||||
const button = document.createElement('button');
|
||||
button.innerHTML = copyIcon;
|
||||
button.className = 'copy-code-btn';
|
||||
button.setAttribute('aria-label', 'Copy code');
|
||||
button.setAttribute('title', 'Copy code');
|
||||
|
||||
// Add the button to the <pre> block
|
||||
// Assuming pre is position relative in CSS
|
||||
pre.appendChild(button);
|
||||
|
||||
button.addEventListener('click', async () => {
|
||||
// Find the <code> element within the <pre>
|
||||
const code = pre.querySelector('code');
|
||||
if (!code) return;
|
||||
|
||||
// Get the text content, removing any leading/trailing whitespace
|
||||
const text = code.innerText;
|
||||
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// Modern Async Clipboard API
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
// Fallback for insecure contexts (like local network IPs via HTTP)
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
// Move outside the screen to make it invisible
|
||||
textArea.style.position = 'absolute';
|
||||
textArea.style.left = '-999999px';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
|
||||
document.execCommand('copy');
|
||||
textArea.remove();
|
||||
}
|
||||
|
||||
// Add visual feedback
|
||||
button.innerHTML = checkIcon;
|
||||
button.classList.add('copied');
|
||||
button.setAttribute('aria-label', 'Copied');
|
||||
button.setAttribute('title', 'Copied!');
|
||||
|
||||
// Revert back after 2 seconds
|
||||
setTimeout(() => {
|
||||
button.innerHTML = copyIcon;
|
||||
button.classList.remove('copied');
|
||||
button.setAttribute('aria-label', 'Copy code');
|
||||
button.setAttribute('title', 'Copy code');
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy code: ', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Vendored
+10
File diff suppressed because one or more lines are too long
@@ -0,0 +1,32 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const refs = document.querySelectorAll('sup.footnote-reference');
|
||||
if (!refs.length) return;
|
||||
|
||||
refs.forEach(function (sup) {
|
||||
const a = sup.querySelector('a');
|
||||
if (!a) return;
|
||||
|
||||
const targetId = a.getAttribute('href').replace(/^#/, '');
|
||||
const refId = 'fnref-' + targetId;
|
||||
sup.id = refId;
|
||||
|
||||
const def = document.getElementById(targetId);
|
||||
if (!def) return;
|
||||
|
||||
// Append ↩ to the last <p> inside the definition
|
||||
const paras = def.querySelectorAll('p');
|
||||
const lastPara = paras[paras.length - 1];
|
||||
if (!lastPara) return;
|
||||
|
||||
const backlink = document.createElement('a');
|
||||
backlink.href = '#' + refId;
|
||||
backlink.className = 'footnote-backref';
|
||||
backlink.setAttribute('aria-label', 'Back to reference');
|
||||
backlink.textContent = '↩';
|
||||
|
||||
lastPara.appendChild(document.createTextNode('\u00a0'));
|
||||
lastPara.appendChild(backlink);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,13 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
document.querySelectorAll('.post-body h2[id], .post-body h3[id]').forEach(function (heading) {
|
||||
var a = document.createElement('a');
|
||||
a.href = '#' + heading.id;
|
||||
a.className = 'heading-anchor';
|
||||
a.setAttribute('aria-label', 'Link to this section');
|
||||
a.setAttribute('tabindex', '-1');
|
||||
a.textContent = '#';
|
||||
heading.appendChild(a);
|
||||
});
|
||||
})();
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+170
@@ -0,0 +1,170 @@
|
||||
!(function (e, t) {
|
||||
'object' == typeof exports && 'object' == typeof module
|
||||
? (module.exports = t(require('katex')))
|
||||
: 'function' == typeof define && define.amd
|
||||
? define(['katex'], t)
|
||||
: 'object' == typeof exports
|
||||
? (exports.renderMathInElement = t(require('katex')))
|
||||
: (e.renderMathInElement = t(e.katex));
|
||||
})('undefined' != typeof self ? self : this, function (e) {
|
||||
return (function () {
|
||||
'use strict';
|
||||
var t = {
|
||||
771: function (t) {
|
||||
t.exports = e;
|
||||
},
|
||||
},
|
||||
r = {};
|
||||
function n(e) {
|
||||
var i = r[e];
|
||||
if (void 0 !== i) return i.exports;
|
||||
var a = (r[e] = { exports: {} });
|
||||
return (t[e](a, a.exports, n), a.exports);
|
||||
}
|
||||
((n.n = function (e) {
|
||||
var t =
|
||||
e && e.__esModule
|
||||
? function () {
|
||||
return e.default;
|
||||
}
|
||||
: function () {
|
||||
return e;
|
||||
};
|
||||
return (n.d(t, { a: t }), t);
|
||||
}),
|
||||
(n.d = function (e, t) {
|
||||
for (var r in t)
|
||||
n.o(t, r) && !n.o(e, r) && Object.defineProperty(e, r, { enumerable: !0, get: t[r] });
|
||||
}),
|
||||
(n.o = function (e, t) {
|
||||
return Object.prototype.hasOwnProperty.call(e, t);
|
||||
}));
|
||||
var i = {};
|
||||
return (
|
||||
(function () {
|
||||
n.d(i, {
|
||||
default: function () {
|
||||
return s;
|
||||
},
|
||||
});
|
||||
var e = n(771),
|
||||
t = n.n(e),
|
||||
r = function (e, t, r) {
|
||||
for (var n = r, i = 0, a = e.length; n < t.length; ) {
|
||||
var o = t[n];
|
||||
if (i <= 0 && t.slice(n, n + a) === e) return n;
|
||||
('\\' === o ? n++ : '{' === o ? i++ : '}' === o && i--, n++);
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
a = /^\\begin{/,
|
||||
o = function (e, t) {
|
||||
for (
|
||||
var n,
|
||||
i = [],
|
||||
o = new RegExp(
|
||||
'(' +
|
||||
t
|
||||
.map(function (e) {
|
||||
return e.left.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
})
|
||||
.join('|') +
|
||||
')',
|
||||
);
|
||||
-1 !== (n = e.search(o));
|
||||
) {
|
||||
n > 0 && (i.push({ type: 'text', data: e.slice(0, n) }), (e = e.slice(n)));
|
||||
var l = t.findIndex(function (t) {
|
||||
return e.startsWith(t.left);
|
||||
});
|
||||
if (-1 === (n = r(t[l].right, e, t[l].left.length))) break;
|
||||
var d = e.slice(0, n + t[l].right.length),
|
||||
s = a.test(d) ? d : e.slice(t[l].left.length, n);
|
||||
(i.push({ type: 'math', data: s, rawData: d, display: t[l].display }),
|
||||
(e = e.slice(n + t[l].right.length)));
|
||||
}
|
||||
return ('' !== e && i.push({ type: 'text', data: e }), i);
|
||||
},
|
||||
l = function (e, r) {
|
||||
var n = o(e, r.delimiters);
|
||||
if (1 === n.length && 'text' === n[0].type) return null;
|
||||
for (var i = document.createDocumentFragment(), a = 0; a < n.length; a++)
|
||||
if ('text' === n[a].type) i.appendChild(document.createTextNode(n[a].data));
|
||||
else {
|
||||
var l = document.createElement('span'),
|
||||
d = n[a].data;
|
||||
r.displayMode = n[a].display;
|
||||
try {
|
||||
(r.preProcess && (d = r.preProcess(d)), t().render(d, l, r));
|
||||
} catch (e) {
|
||||
if (!(e instanceof t().ParseError)) throw e;
|
||||
(r.errorCallback(
|
||||
'KaTeX auto-render: Failed to parse `' + n[a].data + '` with ',
|
||||
e,
|
||||
),
|
||||
i.appendChild(document.createTextNode(n[a].rawData)));
|
||||
continue;
|
||||
}
|
||||
i.appendChild(l);
|
||||
}
|
||||
return i;
|
||||
},
|
||||
d = function e(t, r) {
|
||||
for (var n = 0; n < t.childNodes.length; n++) {
|
||||
var i = t.childNodes[n];
|
||||
if (3 === i.nodeType) {
|
||||
for (
|
||||
var a = i.textContent, o = i.nextSibling, d = 0;
|
||||
o && o.nodeType === Node.TEXT_NODE;
|
||||
)
|
||||
((a += o.textContent), (o = o.nextSibling), d++);
|
||||
var s = l(a, r);
|
||||
if (s) {
|
||||
for (var f = 0; f < d; f++) i.nextSibling.remove();
|
||||
((n += s.childNodes.length - 1), t.replaceChild(s, i));
|
||||
} else n += d;
|
||||
} else
|
||||
1 === i.nodeType &&
|
||||
(function () {
|
||||
var t = ' ' + i.className + ' ';
|
||||
-1 === r.ignoredTags.indexOf(i.nodeName.toLowerCase()) &&
|
||||
r.ignoredClasses.every(function (e) {
|
||||
return -1 === t.indexOf(' ' + e + ' ');
|
||||
}) &&
|
||||
e(i, r);
|
||||
})();
|
||||
}
|
||||
},
|
||||
s = function (e, t) {
|
||||
if (!e) throw new Error('No element provided to render');
|
||||
var r = {};
|
||||
for (var n in t) t.hasOwnProperty(n) && (r[n] = t[n]);
|
||||
((r.delimiters = r.delimiters || [
|
||||
{ left: '$$', right: '$$', display: !0 },
|
||||
{ left: '\\(', right: '\\)', display: !1 },
|
||||
{ left: '\\begin{equation}', right: '\\end{equation}', display: !0 },
|
||||
{ left: '\\begin{align}', right: '\\end{align}', display: !0 },
|
||||
{ left: '\\begin{alignat}', right: '\\end{alignat}', display: !0 },
|
||||
{ left: '\\begin{gather}', right: '\\end{gather}', display: !0 },
|
||||
{ left: '\\begin{CD}', right: '\\end{CD}', display: !0 },
|
||||
{ left: '\\[', right: '\\]', display: !0 },
|
||||
]),
|
||||
(r.ignoredTags = r.ignoredTags || [
|
||||
'script',
|
||||
'noscript',
|
||||
'style',
|
||||
'textarea',
|
||||
'pre',
|
||||
'code',
|
||||
'option',
|
||||
]),
|
||||
(r.ignoredClasses = r.ignoredClasses || []),
|
||||
(r.errorCallback = r.errorCallback || console.error),
|
||||
(r.macros = r.macros || {}),
|
||||
d(e, r));
|
||||
};
|
||||
})(),
|
||||
(i = i.default)
|
||||
);
|
||||
})();
|
||||
});
|
||||
Vendored
+11562
File diff suppressed because it is too large
Load Diff
Vendored
+192132
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const progressBar = document.getElementById('reading-progress-bar');
|
||||
if (!progressBar) return;
|
||||
|
||||
const updateProgress = () => {
|
||||
// Calculate how far the user has scrolled down the page
|
||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
|
||||
// Calculate the total scrollable height
|
||||
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
|
||||
const clientHeight = document.documentElement.clientHeight || window.innerHeight;
|
||||
|
||||
const windowHeight = scrollHeight - clientHeight;
|
||||
|
||||
// Calculate percentage and update width
|
||||
const scrollPercentage = windowHeight > 0 ? (scrollTop / windowHeight) * 100 : 0;
|
||||
progressBar.style.width = scrollPercentage + '%';
|
||||
};
|
||||
|
||||
// Update initially in case the user refreshes midway down the page
|
||||
updateProgress();
|
||||
|
||||
// Throttle the scroll event listener a tiny bit for performance
|
||||
let isTicking = false;
|
||||
window.addEventListener('scroll', () => {
|
||||
if (!isTicking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
updateProgress();
|
||||
isTicking = false;
|
||||
});
|
||||
isTicking = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const scrollToTopBtn = document.getElementById('scroll-to-top');
|
||||
|
||||
if (!scrollToTopBtn) return;
|
||||
|
||||
// Show button when scrolled down 400px
|
||||
const toggleVisibility = () => {
|
||||
if (window.scrollY > 400) {
|
||||
scrollToTopBtn.classList.add('visible');
|
||||
} else {
|
||||
scrollToTopBtn.classList.remove('visible');
|
||||
}
|
||||
};
|
||||
|
||||
// Smooth scroll to top when clicked
|
||||
scrollToTopBtn.addEventListener('click', () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
});
|
||||
|
||||
// Throttle the scroll event listener a tiny bit for performance
|
||||
let isTicking = false;
|
||||
window.addEventListener('scroll', () => {
|
||||
if (!isTicking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
toggleVisibility();
|
||||
isTicking = false;
|
||||
});
|
||||
isTicking = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,223 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const searchToggleBtn = document.querySelector('.search-toggle');
|
||||
if (!searchToggleBtn) return;
|
||||
|
||||
// Check if elasticlunr is available
|
||||
if (typeof elasticlunr === 'undefined') {
|
||||
console.warn('Elasticlunr not loaded. Search will not work.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Modal elements
|
||||
const overlay = document.getElementById('search-overlay');
|
||||
const input = document.getElementById('search-input');
|
||||
const resultsContainer = document.getElementById('search-results');
|
||||
|
||||
if (!overlay || !input || !resultsContainer) return;
|
||||
|
||||
let index = null;
|
||||
let isIndexLoading = false;
|
||||
let selectedIndex = -1;
|
||||
let resultItems = [];
|
||||
|
||||
// Initialize search index (loaded lazily on first open)
|
||||
const initSearch = async () => {
|
||||
if (index || isIndexLoading) return;
|
||||
isIndexLoading = true;
|
||||
|
||||
try {
|
||||
// Fetch the Zola-generated index
|
||||
const response = await fetch('/search_index.en.js');
|
||||
const jsText = await response.text();
|
||||
|
||||
// The file defines `window.searchIndex = {...}`
|
||||
// We use a safe evaluation approach if it's purely assignment
|
||||
// Or just append it as a script tag
|
||||
const script = document.createElement('script');
|
||||
script.innerHTML = jsText;
|
||||
document.body.appendChild(script);
|
||||
|
||||
// Wait for it to parse
|
||||
setTimeout(() => {
|
||||
if (window.searchIndex) {
|
||||
index = elasticlunr.Index.load(window.searchIndex);
|
||||
// Search immediately if there's text
|
||||
if (input.value.trim().length > 0) {
|
||||
performSearch();
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
} catch (e) {
|
||||
console.error('Failed to load search index:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// Open Modal
|
||||
const openSearch = () => {
|
||||
overlay.classList.add('active');
|
||||
|
||||
// Slight delay ensures the browser focuses the input after the display transition starts
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
}, 50);
|
||||
|
||||
initSearch();
|
||||
document.body.style.overflow = 'hidden'; // Prevent background scrolling
|
||||
};
|
||||
|
||||
// Close Modal
|
||||
const closeSearch = () => {
|
||||
overlay.classList.remove('active');
|
||||
input.value = '';
|
||||
resultsContainer.innerHTML = '';
|
||||
document.body.style.overflow = '';
|
||||
selectedIndex = -1;
|
||||
resultItems = [];
|
||||
};
|
||||
|
||||
// Bind Toggle Button
|
||||
searchToggleBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (overlay.classList.contains('active')) {
|
||||
closeSearch();
|
||||
} else {
|
||||
openSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on overlay click
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) closeSearch();
|
||||
});
|
||||
|
||||
// Handle Keyboard Shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Ctrl+K -> Open Search
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
openSearch();
|
||||
}
|
||||
|
||||
if (!overlay.classList.contains('active')) return;
|
||||
|
||||
// Escape -> Close Search
|
||||
if (e.key === 'Escape') {
|
||||
closeSearch();
|
||||
}
|
||||
|
||||
// Arrow Navigation
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (resultItems.length === 0) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
selectedIndex = (selectedIndex + 1) % resultItems.length;
|
||||
} else {
|
||||
selectedIndex = (selectedIndex - 1 + resultItems.length) % resultItems.length;
|
||||
}
|
||||
|
||||
updateSelection();
|
||||
}
|
||||
|
||||
// Enter -> Navigate to selected
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (selectedIndex >= 0 && resultItems[selectedIndex]) {
|
||||
window.location.href = resultItems[selectedIndex].href;
|
||||
} else if (resultItems.length > 0) {
|
||||
// Default to first item if none selected
|
||||
window.location.href = resultItems[0].href;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const updateSelection = () => {
|
||||
resultItems.forEach((el, i) => {
|
||||
if (i === selectedIndex) {
|
||||
el.classList.add('selected');
|
||||
el.scrollIntoView({ block: 'nearest' });
|
||||
} else {
|
||||
el.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Perform the Search
|
||||
const performSearch = () => {
|
||||
if (!index) {
|
||||
resultsContainer.innerHTML = '<div class="no-results">Loading search index...</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const query = input.value.trim();
|
||||
if (query.length === 0) {
|
||||
resultsContainer.innerHTML = '';
|
||||
selectedIndex = -1;
|
||||
resultItems = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
bool: 'OR',
|
||||
expand: true,
|
||||
};
|
||||
|
||||
const results = index.search(query, options);
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsContainer.innerHTML = `<div class="no-results">No results found for "${query}"</div>`;
|
||||
selectedIndex = -1;
|
||||
resultItems = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Build HTML
|
||||
let html = '';
|
||||
results.forEach((res) => {
|
||||
// The doc is stored inside the index.documentStore
|
||||
// For Zola + elasticlunr, res.ref IS the URL.
|
||||
const url = res.ref;
|
||||
const doc = index.documentStore.docs[res.ref];
|
||||
|
||||
if (!doc) return;
|
||||
|
||||
// Truncate summary if needed
|
||||
let body = doc.body || '';
|
||||
if (body.length > 150) {
|
||||
body = body.substring(0, 150) + '...';
|
||||
}
|
||||
|
||||
const title = doc.title || url;
|
||||
|
||||
html += `
|
||||
<a href="${url}" class="search-result-item">
|
||||
<div class="result-title">${title}</div>
|
||||
<div class="result-summary">${body}</div>
|
||||
</a>
|
||||
`;
|
||||
});
|
||||
|
||||
resultsContainer.innerHTML = html;
|
||||
|
||||
// Cache DOM elements for keyboard navigation
|
||||
resultItems = Array.from(resultsContainer.querySelectorAll('.search-result-item'));
|
||||
selectedIndex = -1; // Reset selection on new search
|
||||
|
||||
// Mouse hover updates selection index
|
||||
resultItems.forEach((item, i) => {
|
||||
item.addEventListener('mouseenter', () => {
|
||||
selectedIndex = i;
|
||||
updateSelection();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Debounce the input
|
||||
let debounceTimer;
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
performSearch();
|
||||
}, 150);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
const htmlElement = document.documentElement;
|
||||
|
||||
const applyTheme = (theme) => {
|
||||
if (theme === 'dark') {
|
||||
htmlElement.classList.add('dark');
|
||||
} else {
|
||||
htmlElement.classList.remove('dark');
|
||||
}
|
||||
localStorage.setItem('theme', theme);
|
||||
};
|
||||
|
||||
// Check local storage for theme preference
|
||||
const currentTheme = localStorage.getItem('theme');
|
||||
if (currentTheme === 'dark' || currentTheme === 'light') {
|
||||
applyTheme(currentTheme);
|
||||
} else {
|
||||
// Fallback to system preference
|
||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
applyTheme(prefersDarkScheme.matches ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
// Toggle theme on click
|
||||
if (themeToggleBtn) {
|
||||
themeToggleBtn.addEventListener('click', () => {
|
||||
const nextTheme = htmlElement.classList.contains('dark') ? 'light' : 'dark';
|
||||
applyTheme(nextTheme);
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile nav toggle
|
||||
const navToggle = document.getElementById('nav-toggle');
|
||||
const navLinks = document.getElementById('nav-links');
|
||||
|
||||
if (navToggle && navLinks) {
|
||||
navToggle.addEventListener('click', () => {
|
||||
const isOpen = navLinks.classList.toggle('is-open');
|
||||
navToggle.setAttribute('aria-expanded', isOpen);
|
||||
});
|
||||
|
||||
// Close nav when a link is clicked
|
||||
navLinks.querySelectorAll('.nav-item').forEach((link) => {
|
||||
link.addEventListener('click', () => {
|
||||
navLinks.classList.remove('is-open');
|
||||
navToggle.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Sync cookieconsent dark mode reactively with the site theme
|
||||
const syncCCTheme = () => {
|
||||
const ccMain = document.getElementById('cc-main');
|
||||
if (!ccMain) return;
|
||||
ccMain.classList.toggle('cc--darkmode', htmlElement.classList.contains('dark'));
|
||||
};
|
||||
new MutationObserver(syncCCTheme).observe(htmlElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
// Initial sync in case banner is already mounted
|
||||
setTimeout(syncCCTheme, 100);
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const sidebar = document.querySelector('.post-toc-sidebar');
|
||||
if (!sidebar) return;
|
||||
|
||||
const links = Array.from(sidebar.querySelectorAll('a[data-toc-id]'));
|
||||
if (!links.length) return;
|
||||
|
||||
const headingIds = links.map((a) => a.dataset.tocId);
|
||||
const headings = headingIds.map((id) => document.getElementById(id)).filter(Boolean);
|
||||
|
||||
let activeId = null;
|
||||
|
||||
function setActive(id) {
|
||||
if (id === activeId) return;
|
||||
activeId = id;
|
||||
links.forEach((a) => {
|
||||
if (a.dataset.tocId === id) {
|
||||
a.classList.add('toc-active');
|
||||
} else {
|
||||
a.classList.remove('toc-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
// Find the topmost visible heading
|
||||
const visible = headings.filter((h) => {
|
||||
const rect = h.getBoundingClientRect();
|
||||
return rect.top >= 0 && rect.top < window.innerHeight * 0.6;
|
||||
});
|
||||
|
||||
if (visible.length > 0) {
|
||||
setActive(visible[0].id);
|
||||
} else {
|
||||
// If scrolled past all headings, keep last active
|
||||
const above = headings.filter((h) => h.getBoundingClientRect().top < 0);
|
||||
if (above.length > 0) {
|
||||
setActive(above[above.length - 1].id);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: '-10% 0px -60% 0px',
|
||||
threshold: 0,
|
||||
},
|
||||
);
|
||||
|
||||
headings.forEach((h) => observer.observe(h));
|
||||
|
||||
// Set initial active on load
|
||||
const firstVisible = headings.find((h) => {
|
||||
const rect = h.getBoundingClientRect();
|
||||
return rect.top >= 0;
|
||||
});
|
||||
if (firstVisible) setActive(firstVisible.id);
|
||||
})();
|
||||
Reference in New Issue
Block a user