const scriptEl = document.querySelector('script[data-tool-search]'); const toolsJsonUrl = scriptEl ? new URL('tools.json', scriptEl.src).href : new URL('tools.json', window.location.href).href; const ready = (callback) => { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', callback); } else { callback(); } }; const formatDate = (value) => { if (!value) { return ''; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', }); }; ready(() => { const heading = Array.from(document.querySelectorAll('h1')).find((element) => element.textContent?.trim().toLowerCase().includes('tools.simonwillison.net'), ); if (!heading) { return; } const style = document.createElement('style'); style.textContent = ` .tool-search-container { margin: 1.5rem 0 2rem; padding: 1.15rem 1.5rem 1rem; border-radius: 0.45rem; border: 1.5px solid #d9dce6; background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%); } .tool-search-label { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; } .tool-search-input-wrapper { position: relative; margin-bottom: 0.95rem; } .tool-search-input-icon { position: absolute; top: 50%; left: 1.1rem; transform: translateY(-50%); width: 1rem; height: 1rem; color: #4c5a85; pointer-events: none; } .tool-search-input-icon svg { display: block; width: 100%; height: 100%; } #tool-search-input { width: 100%; box-sizing: border-box; border-radius: 0.7rem; border: 1.5px solid #94a3d4; background-color: rgba(255, 255, 255, 0.95); padding: 0.75rem 1rem 0.75rem 2.75rem; font-size: 1rem; line-height: 1.5; transition: border-color 0.2s ease, box-shadow 0.2s ease; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } #tool-search-input::placeholder { color: #4f5c83; } #tool-search-input:focus { outline: none; border-color: #3b4fd4; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18); background-color: #fff; } #tool-search-input:disabled { color: #6b6f80; background-color: rgba(250, 250, 255, 0.8); } .tool-search-hint { margin: 0 0 0; font-size: 0.875rem; color: #6f7796; max-width: 32rem; } .tool-search-results { list-style: none; padding: 0; margin: 0.75rem 0 0; display: flex; flex-direction: column; gap: 0.5rem; } .tool-search-option { border-radius: 0.7rem; border: 1.5px solid #d8dcff; background: #ffffff; transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.1s ease; } .tool-search-option.active { border-color: #4454f7; transform: translateY(-1px); } .tool-search-option-link { display: flex; flex-direction: column; gap: 0.35rem; text-decoration: none; color: inherit; padding: 0.75rem 0.9rem 0.8rem; } .tool-search-option-link:hover { text-decoration: none; } .tool-search-option-title { font-weight: 600; font-size: 1rem; color: #242847; } .tool-search-option-description { font-size: 0.92rem; color: #3e4261; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .tool-search-option-meta { font-size: 0.82rem; color: #6b6f80; } .tool-search-empty { padding: 0.9rem 1rem; border-radius: 0.7rem; border: 1.5px dashed #c4c8ff; background: rgba(228, 232, 255, 0.5); font-size: 0.95rem; color: #3e4261; } .tool-search-status { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; } @media (max-width: 640px) { .tool-search-container { margin: 1.25rem 0 1.75rem; padding: 0.85rem 0.75rem; } .tool-search-input-icon { left: 0.85rem; } #tool-search-input { font-size: 1.05rem; padding: 0.8rem 0.9rem 0.8rem 2.55rem; } .tool-search-option-link { padding: 0.7rem 0.8rem 0.75rem; } } `; document.head.appendChild(style); const container = document.createElement('section'); container.className = 'tool-search-container'; container.setAttribute('role', 'search'); const label = document.createElement('label'); label.className = 'tool-search-label'; label.setAttribute('for', 'tool-search-input'); label.textContent = 'Search tools'; const inputWrapper = document.createElement('div'); inputWrapper.className = 'tool-search-input-wrapper'; const icon = document.createElement('span'); icon.className = 'tool-search-input-icon'; icon.setAttribute('aria-hidden', 'true'); icon.innerHTML = ` `; const input = document.createElement('input'); input.type = 'search'; input.id = 'tool-search-input'; input.placeholder = 'Loading tools…'; input.autocomplete = 'off'; input.setAttribute('aria-autocomplete', 'list'); input.setAttribute('aria-controls', 'tool-search-results'); input.setAttribute('aria-expanded', 'false'); input.setAttribute('aria-haspopup', 'listbox'); input.setAttribute('role', 'combobox'); input.disabled = true; inputWrapper.appendChild(icon); inputWrapper.appendChild(input); const hint = document.createElement('p'); hint.className = 'tool-search-hint'; hint.textContent = 'Start typing to search all tools. Press “/” to focus the search.'; const results = document.createElement('ul'); results.id = 'tool-search-results'; results.className = 'tool-search-results'; results.setAttribute('role', 'listbox'); results.setAttribute('aria-label', 'Tool suggestions'); results.hidden = true; const status = document.createElement('div'); status.className = 'tool-search-status'; status.setAttribute('role', 'status'); status.setAttribute('aria-live', 'polite'); container.appendChild(label); container.appendChild(inputWrapper); container.appendChild(hint); container.appendChild(results); container.appendChild(status); heading.insertAdjacentElement('afterend', container); let tools = []; let currentMatches = []; let activeIndex = -1; const getVisitCounts = () => { const STORAGE_KEY = 'tools_analytics'; try { const analytics = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); const counts = {}; analytics.forEach((visit) => { // Convert pathname slug (e.g., "/json-to-yaml.html") to tool slug (e.g., "json-to-yaml") let slug = visit.slug || ''; slug = slug.replace(/^\//, '').replace(/\.html$/, ''); if (slug) { counts[slug] = (counts[slug] || 0) + 1; } }); return counts; } catch (e) { return {}; } }; const updateStatus = (message) => { status.textContent = message || ''; }; const clearResults = () => { results.innerHTML = ''; results.hidden = true; input.setAttribute('aria-expanded', 'false'); input.removeAttribute('aria-activedescendant'); currentMatches = []; activeIndex = -1; }; const highlightOption = (index) => { const options = results.querySelectorAll('.tool-search-option'); options.forEach((option) => { option.classList.remove('active'); option.setAttribute('aria-selected', 'false'); }); if (index < 0 || index >= options.length) { input.removeAttribute('aria-activedescendant'); activeIndex = -1; return; } const option = options[index]; option.classList.add('active'); option.setAttribute('aria-selected', 'true'); input.setAttribute('aria-activedescendant', option.id); option.scrollIntoView({ block: 'nearest' }); activeIndex = index; }; const navigateToTool = (tool, { newTab = false } = {}) => { if (!tool) { return; } const destination = tool.url || `${tool.slug}.html`; if (newTab) { window.open(destination, '_blank', 'noopener'); } else { window.location.assign(destination); } }; const renderMatches = (matches, query) => { results.innerHTML = ''; currentMatches = matches.map((entry) => entry.tool); activeIndex = -1; if (!currentMatches.length) { const empty = document.createElement('li'); empty.className = 'tool-search-empty'; empty.textContent = `No tools match “${query}”.`; empty.setAttribute('role', 'option'); empty.setAttribute('aria-selected', 'false'); results.appendChild(empty); results.hidden = false; input.setAttribute('aria-expanded', 'true'); updateStatus(`No tools match ${query}.`); return; } currentMatches.forEach((tool, index) => { const option = document.createElement('li'); option.className = 'tool-search-option'; option.id = `tool-search-option-${index}`; option.setAttribute('role', 'option'); option.setAttribute('aria-selected', 'false'); const link = document.createElement('a'); link.className = 'tool-search-option-link'; link.href = tool.url || `${tool.slug}.html`; link.tabIndex = -1; const title = document.createElement('span'); title.className = 'tool-search-option-title'; title.textContent = tool.title || tool.slug; link.appendChild(title); if (tool.description) { const description = document.createElement('span'); description.className = 'tool-search-option-description'; description.textContent = tool.description; link.appendChild(description); } const metaBits = []; if (tool.updated) { metaBits.push(`Updated ${formatDate(tool.updated)}`); } else if (tool.created) { metaBits.push(`Created ${formatDate(tool.created)}`); } if (metaBits.length) { const meta = document.createElement('span'); meta.className = 'tool-search-option-meta'; meta.textContent = metaBits.join(' • '); link.appendChild(meta); } link.addEventListener('mousedown', (event) => { event.preventDefault(); }); link.addEventListener('click', (event) => { event.preventDefault(); navigateToTool(tool, { newTab: event.metaKey || event.ctrlKey }); }); option.appendChild(link); results.appendChild(option); }); results.hidden = false; input.setAttribute('aria-expanded', 'true'); updateStatus(`${currentMatches.length} result${currentMatches.length === 1 ? '' : 's'} available.`); }; const performSearch = () => { const query = input.value.trim(); if (!query) { clearResults(); updateStatus('Search cleared.'); return; } const lowered = query.toLowerCase(); const terms = lowered.split(/\s+/).filter(Boolean); const visitCounts = getVisitCounts(); const ranked = tools .map((tool) => { const fields = [tool.title, tool.description, tool.slug] .filter(Boolean) .join(' ') .toLowerCase(); if (!terms.every((term) => fields.includes(term))) { return null; } const title = (tool.title || '').toLowerCase(); const slug = (tool.slug || '').toLowerCase(); let score = 100; if (title.startsWith(lowered)) { score = 0; } else if (slug.startsWith(lowered)) { score = 10; } else if (title.includes(lowered)) { score = 20; } else if (slug.includes(lowered)) { score = 30; } const updated = tool.updated ? Date.parse(tool.updated) || 0 : 0; const visits = visitCounts[tool.slug] || 0; return { tool, score, updated, visits }; }) .filter(Boolean) .sort((a, b) => { // Sort by visits first (descending - more visits is better) if (a.visits !== b.visits) { return b.visits - a.visits; } // Then by score (ascending - lower is better) if (a.score !== b.score) { return a.score - b.score; } // Then by updated date (descending - newer is better) return b.updated - a.updated; }) .slice(0, 12); renderMatches(ranked, query); }; input.addEventListener('input', () => { if (!tools.length) { return; } performSearch(); }); input.addEventListener('keydown', (event) => { if (!currentMatches.length && !['Escape', 'Tab'].includes(event.key)) { return; } if (event.key === 'ArrowDown') { event.preventDefault(); if (!results.hidden) { const nextIndex = (activeIndex + 1) % currentMatches.length; highlightOption(nextIndex); } else { performSearch(); } } else if (event.key === 'ArrowUp') { event.preventDefault(); const nextIndex = activeIndex <= 0 ? currentMatches.length - 1 : activeIndex - 1; highlightOption(nextIndex); } else if (event.key === 'Enter') { if (!currentMatches.length) { return; } event.preventDefault(); const chosen = activeIndex >= 0 ? currentMatches[activeIndex] : currentMatches[0]; navigateToTool(chosen, { newTab: event.metaKey || event.ctrlKey }); } else if (event.key === 'Escape') { clearResults(); input.blur(); } }); input.addEventListener('focus', () => { if (input.value && currentMatches.length) { results.hidden = false; input.setAttribute('aria-expanded', 'true'); } }); input.addEventListener('blur', () => { window.setTimeout(() => { clearResults(); }, 120); }); document.addEventListener('click', (event) => { if (!container.contains(event.target)) { clearResults(); } }); document.addEventListener('keydown', (event) => { if (event.key !== '/' || event.altKey || event.ctrlKey || event.metaKey) { return; } const target = event.target; const tagName = target?.tagName?.toLowerCase(); const isEditable = target?.isContentEditable; if (tagName === 'input' || tagName === 'textarea' || tagName === 'select' || isEditable) { return; } event.preventDefault(); input.focus(); input.select(); }); fetch(toolsJsonUrl) .then((response) => { if (!response.ok) { throw new Error(`Failed to load tools.json: ${response.status}`); } return response.json(); }) .then((data) => { if (!Array.isArray(data)) { throw new Error('tools.json did not return an array'); } tools = data; input.placeholder = `Search ${tools.length} tools…`; input.disabled = false; updateStatus(`${tools.length} tools available to search.`); if (input === document.activeElement && input.value) { performSearch(); } }) .catch((error) => { console.error(error); input.placeholder = 'Search unavailable'; input.disabled = true; updateStatus('Search unavailable.'); }); });