From 843ad379c799b789b24e5e3f55b656d1a3aefb76 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sun, 1 Mar 2026 23:37:51 -0500 Subject: [PATCH] Add search functionality across documentation pages - Implemented a search button in the header of each documentation page. - Added a search modal that allows users to input search queries. - Integrated a JavaScript search feature that fetches and displays results from a new `pages-data.json` file. - Each documentation page now includes a search overlay for improved navigation. - Updated the main JavaScript file to handle search logic, including result highlighting and navigation. - Created a `pages-data.json` file containing metadata for all documentation pages to facilitate search functionality. --- docs-gen/defaults/static/css/main.css | 145 ++++++++++++ docs-gen/defaults/static/js/main.js | 217 ++++++++++++++++++ docs-gen/defaults/templates/base.html | 15 ++ docs-gen/src/builder.rs | 54 +++++ docs/css/main.css | 145 ++++++++++++ docs/en/cookbook/index.html | 15 ++ docs/en/index.html | 15 ++ docs/en/tour/01-getting-started/index.html | 15 ++ docs/en/tour/02-basic-client/index.html | 15 ++ docs/en/tour/03-basic-server/index.html | 15 ++ docs/en/tour/04-static-file-server/index.html | 15 ++ docs/en/tour/05-tls-setup/index.html | 15 ++ docs/en/tour/06-https-client/index.html | 15 ++ docs/en/tour/07-https-server/index.html | 15 ++ docs/en/tour/08-websocket/index.html | 15 ++ docs/en/tour/09-whats-next/index.html | 15 ++ docs/en/tour/index.html | 15 ++ docs/ja/cookbook/index.html | 15 ++ docs/ja/index.html | 15 ++ docs/ja/tour/01-getting-started/index.html | 15 ++ docs/ja/tour/02-basic-client/index.html | 15 ++ docs/ja/tour/03-basic-server/index.html | 15 ++ docs/ja/tour/04-static-file-server/index.html | 15 ++ docs/ja/tour/05-tls-setup/index.html | 15 ++ docs/ja/tour/06-https-client/index.html | 15 ++ docs/ja/tour/07-https-server/index.html | 15 ++ docs/ja/tour/08-websocket/index.html | 15 ++ docs/ja/tour/09-whats-next/index.html | 15 ++ docs/ja/tour/index.html | 15 ++ docs/js/main.js | 217 ++++++++++++++++++ docs/pages-data.json | 1 + 31 files changed, 1154 insertions(+) create mode 100644 docs/pages-data.json diff --git a/docs-gen/defaults/static/css/main.css b/docs-gen/defaults/static/css/main.css index 0f2ded0..cd99f44 100644 --- a/docs-gen/defaults/static/css/main.css +++ b/docs-gen/defaults/static/css/main.css @@ -450,3 +450,148 @@ a:hover { .theme-toggle:hover { opacity: 1; } + +/* Search button */ +.search-btn { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + color: var(--text); + padding: 5px 6px; + border-radius: 4px; + cursor: pointer; + opacity: 0.8; +} + +.search-btn:hover { + opacity: 1; +} + +/* Search overlay & modal */ +.search-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 300; + justify-content: center; + align-items: flex-start; + padding-top: 12vh; +} + +.search-overlay.open { + display: flex; +} + +.search-modal { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + width: 90%; + max-width: 560px; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); + overflow: hidden; +} + +.search-input-wrap { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + border-bottom: 1px solid var(--border); +} + +.search-input-wrap svg { + flex-shrink: 0; + opacity: 0.5; +} + +#search-input { + flex: 1; + background: none; + border: none; + outline: none; + color: var(--text-bright); + font-size: 1rem; + font-family: inherit; +} + +#search-input::placeholder { + color: var(--text-muted); +} + +.search-esc { + background: var(--bg); + color: var(--text-muted); + padding: 2px 6px; + border-radius: 3px; + font-size: 0.7rem; + border: 1px solid var(--border); +} + +.search-results { + list-style: none; + max-height: 50vh; + overflow-y: auto; + padding: 0; + margin: 0; +} + +.search-results:empty::after { + content: ""; + display: none; +} + +.search-results li { + padding: 10px 16px; + cursor: pointer; + border-bottom: 1px solid var(--border); +} + +.search-results li:last-child { + border-bottom: none; +} + +.search-results li:hover, +.search-results li.active { + background: var(--bg); +} + +.search-results li .search-result-title { + color: var(--text-bright); + font-weight: 600; + font-size: 0.9rem; + margin-bottom: 2px; +} + +.search-results li .search-result-snippet { + color: var(--text-muted); + font-size: 0.8rem; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.search-results li .search-result-snippet mark { + background: rgba(255, 215, 0, 0.3); + color: var(--text-bright); + border-radius: 2px; + padding: 0 1px; +} + +[data-theme="light"] .search-results li .search-result-snippet mark { + background: rgba(255, 200, 0, 0.4); + color: var(--text); +} + +.search-no-results { + padding: 24px 16px; + text-align: center; + color: var(--text-muted); + font-size: 0.9rem; +} diff --git a/docs-gen/defaults/static/js/main.js b/docs-gen/defaults/static/js/main.js index ae0034b..0f2a89f 100644 --- a/docs-gen/defaults/static/js/main.js +++ b/docs-gen/defaults/static/js/main.js @@ -78,3 +78,220 @@ } }); })(); + +// Site search (⌘K / Ctrl+K) +(function () { + var overlay = document.getElementById('search-overlay'); + var input = document.getElementById('search-input'); + var resultsList = document.getElementById('search-results'); + if (!overlay || !input || !resultsList) return; + + var searchBtn = document.querySelector('.search-btn'); + var pagesData = null; // cached pages-data.json + var activeIndex = -1; + + function getCurrentLang() { + return document.documentElement.getAttribute('lang') || 'en'; + } + + function getBasePath() { + return document.documentElement.getAttribute('data-base-path') || ''; + } + + function openSearch() { + overlay.classList.add('open'); + input.value = ''; + resultsList.innerHTML = ''; + activeIndex = -1; + input.focus(); + loadPagesData(); + } + + function closeSearch() { + overlay.classList.remove('open'); + input.value = ''; + resultsList.innerHTML = ''; + activeIndex = -1; + } + + function loadPagesData() { + if (pagesData) return; + var basePath = getBasePath(); + fetch(basePath + '/pages-data.json') + .then(function (res) { return res.json(); }) + .then(function (data) { pagesData = data; }) + .catch(function () { pagesData = []; }); + } + + function escapeRegExp(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + function highlightText(text, query) { + if (!query) return text; + var escaped = escapeRegExp(query); + var re = new RegExp('(' + escaped + ')', 'gi'); + return text.replace(re, '$1'); + } + + function buildSnippet(body, query) { + if (!query || !body) return ''; + var lower = body.toLowerCase(); + var idx = lower.indexOf(query.toLowerCase()); + var start, end, snippet; + if (idx === -1) { + snippet = body.substring(0, 120); + } else { + start = Math.max(0, idx - 40); + end = Math.min(body.length, idx + query.length + 80); + snippet = (start > 0 ? '...' : '') + body.substring(start, end) + (end < body.length ? '...' : ''); + } + return highlightText(snippet, query); + } + + function search(query) { + if (!pagesData || !query) { + resultsList.innerHTML = ''; + activeIndex = -1; + return; + } + + var lang = getCurrentLang(); + var q = query.toLowerCase(); + + // Score and filter + var scored = []; + for (var i = 0; i < pagesData.length; i++) { + var page = pagesData[i]; + if (page.lang !== lang) continue; + + var score = 0; + var titleLower = page.title.toLowerCase(); + var bodyLower = (page.body || '').toLowerCase(); + + if (titleLower.indexOf(q) !== -1) { + score += 10; + // Bonus for exact title match + if (titleLower === q) score += 5; + } + if (bodyLower.indexOf(q) !== -1) { + score += 3; + } + if (page.section.toLowerCase().indexOf(q) !== -1) { + score += 1; + } + + if (score > 0) { + scored.push({ page: page, score: score }); + } + } + + // Sort by score descending + scored.sort(function (a, b) { return b.score - a.score; }); + + // Limit results + var results = scored.slice(0, 20); + + if (results.length === 0) { + resultsList.innerHTML = '
  • No results found.
  • '; + activeIndex = -1; + return; + } + + var html = ''; + for (var j = 0; j < results.length; j++) { + var r = results[j]; + var snippet = buildSnippet(r.page.body, query); + html += '
  • ' + + '
    ' + highlightText(r.page.title, query) + '
    ' + + (snippet ? '
    ' + snippet + '
    ' : '') + + '
  • '; + } + resultsList.innerHTML = html; + activeIndex = -1; + } + + function setActive(index) { + var items = resultsList.querySelectorAll('li[data-url]'); + if (items.length === 0) return; + // Remove previous active + for (var i = 0; i < items.length; i++) { + items[i].classList.remove('active'); + } + if (index < 0) index = items.length - 1; + if (index >= items.length) index = 0; + activeIndex = index; + items[activeIndex].classList.add('active'); + items[activeIndex].scrollIntoView({ block: 'nearest' }); + } + + function navigateToActive() { + var items = resultsList.querySelectorAll('li[data-url]'); + if (activeIndex >= 0 && activeIndex < items.length) { + var url = items[activeIndex].getAttribute('data-url'); + if (url) { + closeSearch(); + window.location.href = url; + } + } + } + + // Event: search button + if (searchBtn) { + searchBtn.addEventListener('click', function (e) { + e.stopPropagation(); + openSearch(); + }); + } + + // Use capture phase to intercept keys before browser default behavior + // (e.g. ESC clearing input text in some browsers) + document.addEventListener('keydown', function (e) { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + overlay.classList.contains('open') ? closeSearch() : openSearch(); + return; + } + if (!overlay.classList.contains('open')) return; + if (e.key === 'Escape') { + e.preventDefault(); + closeSearch(); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + setActive(activeIndex + 1); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setActive(activeIndex - 1); + } else if (e.key === 'Enter') { + e.preventDefault(); + navigateToActive(); + } + }, true); // capture phase + + // Event: click overlay background to close + overlay.addEventListener('click', function (e) { + if (e.target === overlay) { + closeSearch(); + } + }); + + // Event: click result item + resultsList.addEventListener('click', function (e) { + var li = e.target.closest('li[data-url]'); + if (!li) return; + var url = li.getAttribute('data-url'); + if (url) { + closeSearch(); + window.location.href = url; + } + }); + + // Event: input for live search + var debounceTimer = null; + input.addEventListener('input', function () { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(function () { + search(input.value.trim()); + }, 150); + }); +})(); diff --git a/docs-gen/defaults/templates/base.html b/docs-gen/defaults/templates/base.html index 153377b..0445013 100644 --- a/docs-gen/defaults/templates/base.html +++ b/docs-gen/defaults/templates/base.html @@ -39,6 +39,9 @@ {% endfor %}
    +