marked/docs/js/index.js
document.addEventListener('DOMContentLoaded', function() {
// --- Theme Toggling ---
const themeToggle = document.getElementById('theme-toggle');
const themeToggleIcon = themeToggle ? themeToggle.querySelector('[data-theme-icon]') : null;
const themeToggleText = themeToggle ? themeToggle.querySelector('[data-theme-text]') : null;
const THEME_STORAGE_KEY = 'theme-preference';
const LEGACY_STORAGE_KEY = 'theme';
const THEME_ORDER = ['system', 'light', 'dark'];
const TOGGLE_UI = {
system: { icon: 'brightness_auto', text: 'System' },
light: { icon: 'light_mode', text: 'Light' },
dark: { icon: 'dark_mode', text: 'Dark' },
};
function applyTheme(theme) {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
document.documentElement.setAttribute('data-theme', theme);
}
function getSystemTheme() {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
function sanitisePreference(value) {
return THEME_ORDER.includes(value) ? value : null;
}
function readStoredPreference() {
try {
const stored = sanitisePreference(localStorage.getItem(THEME_STORAGE_KEY));
if (stored) {
return stored;
}
return sanitisePreference(localStorage.getItem(LEGACY_STORAGE_KEY));
} catch {
return null;
}
}
function writeStoredPreference(preference) {
try {
localStorage.setItem(THEME_STORAGE_KEY, preference);
if (preference === 'light' || preference === 'dark') {
localStorage.setItem(LEGACY_STORAGE_KEY, preference);
} else {
localStorage.removeItem(LEGACY_STORAGE_KEY);
}
} catch {
// Storage might be unavailable; ignore
}
}
function getEffectiveTheme(preference) {
return preference === 'system' ? getSystemTheme() : preference;
}
function updateToggle(preference) {
if (!themeToggle) {
return;
}
const details = TOGGLE_UI[preference] || TOGGLE_UI.system;
if (themeToggleIcon) {
themeToggleIcon.textContent = details.icon;
}
if (themeToggleText) {
themeToggleText.textContent = details.text;
}
themeToggle.setAttribute('data-theme-mode', preference);
const label = `Switch theme (current: ${details.text})`;
themeToggle.setAttribute('aria-label', label);
themeToggle.title = label;
}
let currentPreference = readStoredPreference() || 'system';
function applyPreference(preference, persist) {
currentPreference = preference;
const effectiveTheme = getEffectiveTheme(preference);
applyTheme(effectiveTheme);
document.documentElement.setAttribute('data-theme-preference', preference);
updateToggle(preference);
if (persist) {
writeStoredPreference(preference);
}
}
applyPreference(currentPreference, true);
if (themeToggle) {
themeToggle.addEventListener('click', function() {
const index = THEME_ORDER.indexOf(currentPreference);
const nextIndex = index === -1 ? 0 : (index + 1) % THEME_ORDER.length;
const nextPreference = THEME_ORDER[nextIndex];
applyPreference(nextPreference, true);
});
}
const systemMatcher = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
if (systemMatcher) {
const handleSystemChange = function() {
if (currentPreference === 'system') {
applyPreference('system', false);
}
};
if (typeof systemMatcher.addEventListener === 'function') {
systemMatcher.addEventListener('change', handleSystemChange);
} else if (typeof systemMatcher.addListener === 'function') {
systemMatcher.addListener(handleSystemChange);
}
}
// --- Copy-to-Clipboard Button ---
const allPres = document.querySelectorAll('pre');
allPres.forEach(function(pre) {
let timeout = null;
const copyButton = document.createElement('button');
copyButton.className =
'absolute top-2 right-2 p-2 rounded-md bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 opacity-0 group-hover:opacity-100 transition-opacity';
copyButton.innerHTML =
'<span class="material-icons text-sm">content_copy</span>';
copyButton.setAttribute('aria-label', 'Copy to clipboard');
pre.classList.add('group', 'relative'); // Add group for hover effect
pre.appendChild(copyButton);
copyButton.onclick = function() {
// Exclude the button's own text from being copied
const code = pre.querySelector('code').innerText;
navigator.clipboard.writeText(code);
copyButton.innerHTML = '<span class="material-icons text-sm">done</span>';
clearTimeout(timeout);
timeout = setTimeout(function() {
copyButton.innerHTML =
'<span class="material-icons text-sm">content_copy</span>';
}, 2000);
};
});
// --- LEGACY URL Redirect ---
const match = /#\/(.+)\\.md(.*)/g.exec(window.location.hash);
if (match && match[1]) {
const pageName = match[1].toLowerCase();
const sectionName = match[2];
window.location.href = '/' + pageName + sectionName;
}
// --- Mobile Menu Toggle ---
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
const sidebar = document.getElementById('sidebar');
const mobileOverlay = document.getElementById('mobile-overlay');
const body = document.body;
function openMobileMenu() {
sidebar.classList.add('mobile-open');
mobileOverlay.classList.add('active');
body.classList.add('mobile-menu-open');
mobileMenuToggle.setAttribute('aria-expanded', 'true');
}
function closeMobileMenu() {
sidebar.classList.remove('mobile-open');
mobileOverlay.classList.remove('active');
body.classList.remove('mobile-menu-open');
mobileMenuToggle.setAttribute('aria-expanded', 'false');
}
if (mobileMenuToggle) {
mobileMenuToggle.addEventListener('click', function() {
const isOpen = sidebar.classList.contains('mobile-open');
if (isOpen) {
closeMobileMenu();
} else {
openMobileMenu();
}
});
}
if (mobileOverlay) {
mobileOverlay.addEventListener('click', closeMobileMenu);
}
// Close mobile menu when clicking a navigation link
if (sidebar) {
const sidebarLinks = sidebar.querySelectorAll('a');
sidebarLinks.forEach(function(link) {
link.addEventListener('click', function() {
// Only close on mobile/tablet (up to 1024px)
if (window.innerWidth <= 1024) {
closeMobileMenu();
}
});
});
}
// Close mobile menu on window resize to desktop size
window.addEventListener('resize', function() {
if (window.innerWidth > 1024) {
closeMobileMenu();
}
});
// --- Known Extensions Search ---
const searchInput = document.getElementById('extension-search');
if (searchInput) {
const noResultsMsg = document.getElementById('no-extensions-msg');
const clearButton = document.getElementById('extension-search-clear');
const extensionsHeading = document.getElementById('extensions');
let table = null;
if (extensionsHeading) {
let sibling = extensionsHeading.nextElementSibling;
while (sibling) {
if (sibling.tagName === 'TABLE') {
table = sibling;
break;
}
sibling = sibling.nextElementSibling;
}
}
if (table) {
const tableRows = table.querySelectorAll('tbody tr');
function filterExtensions() {
const query = searchInput.value.toLowerCase().trim();
let hasRows = false;
clearButton.classList.toggle('hidden', !query);
for (const row of tableRows) {
const cells = row.querySelectorAll('td');
const name = cells[0]?.textContent?.toLowerCase() ?? '';
const packageName = cells[1]?.textContent?.toLowerCase() ?? '';
const description = cells[2]?.textContent?.toLowerCase() ?? '';
const includesQuery = name.includes(query) || packageName.includes(query) || description.includes(query);
row.classList.toggle('hidden', !includesQuery);
hasRows = hasRows || includesQuery;
}
// Toggle table display and "no results" message based on matches
table.classList.toggle('hidden', !hasRows);
noResultsMsg.classList.toggle('hidden', hasRows);
}
const debouncedFilter = debounce(filterExtensions, 200);
searchInput.addEventListener('input', debouncedFilter);
clearButton.addEventListener('click', function() {
searchInput.value = '';
debouncedFilter.cancel();
filterExtensions();
searchInput.focus();
});
}
}
});
function debounce(func, wait) {
let timeout;
const debounced = (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(args), wait);
};
debounced.cancel = () => {
clearTimeout(timeout);
};
return debounced;
}