Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 39 additions & 24 deletions theme/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -363,34 +363,49 @@ <h2 class="accordion-header" id="h-{{ pid }}">
<!-- ========================= /NAV ========================= -->


<main>
<div class="container-xxl">
<section class="content-inner">
{% block content %}
<section>
<div class="row">
<div class="col-12 col-xl-10 mx-auto">
{% block content_inner %}
{{ page.content }}
{% endblock content_inner %}
<main>
<div class="container-xxl">
<section class="content-inner">
{% block content %}
<section>
<div class="row">
<div class="col-12 col-xl-10 mx-auto">
{% block content_inner %}

<!-- Listen to article button -->
<div class="mb-3">
<button
id="tts-btn"
type="button"
class="btn btn-sm btn-outline-secondary"
aria-label="Listen to article"
>
Listen to article
</button>
</div>
</div>
</section>
{% endblock content %}

<div class="mt-4">
<small class="badge-updated">
{% if page.meta.git_revision_date_localized %}
Last update: {{ page.meta.git_revision_date_localized }}
{% endif %}
{% if page.meta.git_created_date_localized %}
&nbsp;•&nbsp;Created: {{ page.meta.git_created_date_localized }}
{% endif %}
</small>
{{ page.content }}

{% endblock content_inner %}
</div>
</section>
</div>
</section>
{% endblock content %}

<div class="mt-4">
<small class="badge-updated">
{% if page.meta.git_revision_date_localized %}
Last update: {{ page.meta.git_revision_date_localized }}
{% endif %}
{% if page.meta.git_created_date_localized %}
&nbsp;•&nbsp;Created: {{ page.meta.git_created_date_localized }}
{% endif %}
</small>
</div>
</main>
</section>
</div>
</main>


<svg width="0" height="0" class="hidden">
<symbol viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" id="facebook">
Expand Down
83 changes: 57 additions & 26 deletions theme/js/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,35 +55,71 @@
if (offcanvasEl) {
offcanvasEl.addEventListener('click', function (e) {
const a = e.target.closest('a');
if (!a) return; // ignore clicks on accordion buttons etc.
if (!a) return;
const offcanvas = bootstrap.Offcanvas.getInstance(offcanvasEl) ||
new bootstrap.Offcanvas(offcanvasEl);
// Close for internal links (no target="_blank")
if (a.getAttribute('href') && !a.getAttribute('target')) {
offcanvas.hide();
}
});
}

/* 5) Mega dropdown accordions: ensure one-open-at-a-time via data-bs-parent */
/* 5) Mega dropdown accordions */
document.querySelectorAll('.dropdown-menu.mega .accordion').forEach((acc, i) => {
// Make sure the accordion has an id
if (!acc.id) acc.id = `mega-acc-${i}`;
const parentSel = `#${acc.id}`;

// For each collapse pane, set data-bs-parent if missing
acc.querySelectorAll('.accordion-collapse').forEach(col => {
if (!col.getAttribute('data-bs-parent')) {
col.setAttribute('data-bs-parent', parentSel);
}
});
});

/* 6) Keep clicks inside mega from bubbling to the dropdown toggle (belt & suspenders).
With data-bs-auto-close="outside" this isn’t strictly needed, but it’s harmless. */
/* 6) Stop mega menu click bubbling */
document.querySelectorAll('.dropdown-menu.mega').forEach(menu => {
menu.addEventListener('click', (e) => e.stopPropagation());
});

/* =========================================================
7) Client-side Text-to-Speech (Listen to Article)
========================================================= */
if ('speechSynthesis' in window) {
const ttsBtn = document.getElementById('tts-btn');
let speaking = false;
let utterance = null;

function getArticleText() {
const main = document.querySelector('main');
return main ? main.innerText : '';
}

if (ttsBtn) {
ttsBtn.addEventListener('click', function () {
if (speaking) {
window.speechSynthesis.cancel();
speaking = false;
ttsBtn.textContent = 'Listen to article';
return;
}

const text = getArticleText();
if (!text) return;

utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'en-US';
utterance.rate = 1;

utterance.onend = function () {
speaking = false;
ttsBtn.textContent = 'Listen to article';
};

window.speechSynthesis.speak(utterance);
speaking = true;
ttsBtn.textContent = 'Stop listening';
});
}
}
});

// Optional public API
Expand All @@ -99,17 +135,15 @@
document.querySelectorAll('a[href^="http"]').forEach(a => {
try {
const url = new URL(a.href);
// skip same-origin
if (url.origin === window.location.origin) return;
if (!a.hasAttribute('target')) a.setAttribute('target', '_blank');
if (!a.hasAttribute('rel')) a.setAttribute('rel', 'noopener');
} catch (_) {}
});

// B) small “copy code buttons for pygments blocks (MkDocs default markup)
// B) copy code buttons
document.querySelectorAll('div.highlight > pre').forEach((pre, i) => {
// container for the button
const wrap = pre.parentElement; // .highlight
const wrap = pre.parentElement;
wrap.style.position = 'relative';

const btn = document.createElement('button');
Expand All @@ -126,7 +160,6 @@
btn.textContent = 'Copied!';
setTimeout(() => (btn.textContent = old), 1200);
} catch (e) {
// fallback
const ta = document.createElement('textarea');
ta.value = code;
ta.style.position = 'fixed';
Expand All @@ -144,18 +177,17 @@
wrap.appendChild(btn);
});

// C) heading anchors (h2–h4) inside main content
// C) heading anchors
const contentRoot = document.querySelector('main .content-inner') || document.querySelector('main');
if (contentRoot) {
contentRoot.querySelectorAll('h2[id], h3[id], h4[id]').forEach(h => {
if (h.querySelector('a.anchor-link')) return; // idempotent
if (h.querySelector('a.anchor-link')) return;
const a = document.createElement('a');
a.href = `#${h.id}`;
a.className = 'anchor-link ms-2';
a.setAttribute('aria-label', 'Copy link to this section');
a.innerHTML = '¶'; // simple mark; you can swap for an SVG if you prefer
a.addEventListener('click', (e) => {
// let it navigate, then copy
a.innerHTML = '¶';
a.addEventListener('click', () => {
setTimeout(() => navigator.clipboard.writeText(window.location.href), 0);
});
h.appendChild(a);
Expand All @@ -167,7 +199,6 @@

// --- Search results link absolutizer ---
(function () {
// Which containers might hold search result links?
const candidates = [
'.mk-search-results',
'.search-results',
Expand All @@ -176,31 +207,31 @@

function absolutize(href) {
if (!href) return href;
if (/^([a-z]+:)?\/\//i.test(href)) return href; // already absolute URL
if (href.startsWith('/')) return href; // already site-absolute
return '/' + href.replace(/^\/+/, ''); // make it site-absolute
if (/^([a-z]+:)?\/\//i.test(href)) return href;
if (href.startsWith('/')) return href;
return '/' + href.replace(/^\/+/, '');
}

function fixLinks(root = document) {
candidates.forEach(sel => {
root.querySelectorAll(`${sel} a[href]`).forEach(a => {
const fixed = absolutize(a.getAttribute('href'));
if (fixed && fixed !== a.getAttribute('href')) a.setAttribute('href', fixed);
if (fixed && fixed !== a.getAttribute('href')) {
a.setAttribute('href', fixed);
}
});
});
}

// 1) Run once after DOM ready (in case results render immediately)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => fixLinks());
} else {
fixLinks();
}

// 2) Watch for results being (re)rendered
const obs = new MutationObserver(muts => {
for (const m of muts) {
if (m.type === 'childList' && (m.addedNodes && m.addedNodes.length)) {
if (m.type === 'childList' && m.addedNodes.length) {
fixLinks(document);
}
}
Expand Down
Loading