From 75c414095392b89a6f7e93287b6682b577fba038 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:32:49 -0500 Subject: [PATCH 01/23] Create smart_search.rst --- docs/smart_search.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/smart_search.rst diff --git a/docs/smart_search.rst b/docs/smart_search.rst new file mode 100644 index 000000000..f467eab61 --- /dev/null +++ b/docs/smart_search.rst @@ -0,0 +1,18 @@ +Smart Search +============ + +Type an exact ProDy function name (for example ``parsePDB``). +If an exact match exists, you will be redirected directly to that function. +Otherwise, normal search results will be shown. + +.. raw:: html + + + + + + From f46a4c42201f82d67e3a4c798327042d535da1d0 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:33:31 -0500 Subject: [PATCH 02/23] Create smart_search.js --- docs/_static/smart_search.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/_static/smart_search.js diff --git a/docs/_static/smart_search.js b/docs/_static/smart_search.js new file mode 100644 index 000000000..7ddf541fd --- /dev/null +++ b/docs/_static/smart_search.js @@ -0,0 +1,28 @@ +async function smartSearch() { + const q = document.getElementById("smart-search-input").value.trim(); + if (!q) return; + + // where RTD stores search index + const base = window.location.pathname.split("/").slice(0,3).join("/"); + const indexUrl = base + "/_static/api_index.json"; + + try { + const res = await fetch(indexUrl); + const data = await res.json(); + + // EXACT match only + if (data[q] && data[q].length > 0) { + const target = data[q][0]; + const parts = target.split("."); + const mod = parts.slice(1,-1).join("/"); + window.location.href = + base + "/reference/" + mod + ".html#" + target; + return; + } + } catch (e) { + console.warn("Smart search fallback:", e); + } + + // fallback to normal RTD search + window.location.href = base + "/search.html?q=" + encodeURIComponent(q); +} From 5b1b2963671805a28206c3e089b4174189e1ae52 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:35:13 -0500 Subject: [PATCH 03/23] Update index.rst --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index a388f3b99..6fb6a0a20 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,8 +17,8 @@ ProDy Manual .. toctree:: :maxdepth: 2 - :glob: + smart_search getprody apps/index reference/index From ce7c5d591ec395ab08a3ac8fc8d45b5db0ce026b Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:47:36 -0500 Subject: [PATCH 04/23] Update smart_search.js --- docs/_static/smart_search.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/_static/smart_search.js b/docs/_static/smart_search.js index 7ddf541fd..bd42884ce 100644 --- a/docs/_static/smart_search.js +++ b/docs/_static/smart_search.js @@ -26,3 +26,14 @@ async function smartSearch() { // fallback to normal RTD search window.location.href = base + "/search.html?q=" + encodeURIComponent(q); } +document.addEventListener("DOMContentLoaded", () => { + const input = document.getElementById("smart-search-input"); + if (!input) return; + + input.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + smartSearch(); + } + }); +}); From 02576af5d3a4b368443195cdf9099be68e5bb178 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:05:55 -0500 Subject: [PATCH 05/23] Update conf.py --- docs/conf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 5efe33e52..77e7c74cf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,7 +73,13 @@ # Ensure this is set so Sphinx looks in the _static folder html_static_path = ['_static'] +html_js_files = [ + "smart_search.js", +] +html_css_files = [ + "smart_search.css", +] # -- Extension Configuration ------------------------------------------------- # 1. Napoleon Settings (for NumPy docstrings) From e2f353ac08cdb0f9dbb69b1a64dec951b8261f4d Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:06:21 -0500 Subject: [PATCH 06/23] Update smart_search.js --- docs/_static/smart_search.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/_static/smart_search.js b/docs/_static/smart_search.js index bd42884ce..0dff374d6 100644 --- a/docs/_static/smart_search.js +++ b/docs/_static/smart_search.js @@ -37,3 +37,17 @@ document.addEventListener("DOMContentLoaded", () => { } }); }); +document.addEventListener("DOMContentLoaded", function () { + const form = document.getElementById("smartSearchForm"); + const input = document.getElementById("smartSearchInput"); + + if (!form || !input) return; + + input.addEventListener("keydown", function (e) { + if (e.key === "Enter") { + e.preventDefault(); + if (typeof form.requestSubmit === "function") form.requestSubmit(); + else form.dispatchEvent(new Event("submit", { cancelable: true })); + } + }); +}); From e6159bd265bf537c1fc4e4c315e3dcfd2215aa9f Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:09:13 -0500 Subject: [PATCH 07/23] Update smart_search.rst --- docs/smart_search.rst | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/smart_search.rst b/docs/smart_search.rst index f467eab61..8d8d3b1ed 100644 --- a/docs/smart_search.rst +++ b/docs/smart_search.rst @@ -2,17 +2,14 @@ Smart Search ============ Type an exact ProDy function name (for example ``parsePDB``). -If an exact match exists, you will be redirected directly to that function. -Otherwise, normal search results will be shown. .. raw:: html - - - +
+ + +
From b9e07228568e9cbef77ab8f21b51156875859b7a Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:09:55 -0500 Subject: [PATCH 08/23] Update smart_search.js --- docs/_static/smart_search.js | 46 +++++++++--------------------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/docs/_static/smart_search.js b/docs/_static/smart_search.js index 0dff374d6..9180dcf58 100644 --- a/docs/_static/smart_search.js +++ b/docs/_static/smart_search.js @@ -1,53 +1,29 @@ -async function smartSearch() { - const q = document.getElementById("smart-search-input").value.trim(); +function runSmartSearch(query) { + const q = query.trim(); if (!q) return; - // where RTD stores search index - const base = window.location.pathname.split("/").slice(0,3).join("/"); - const indexUrl = base + "/_static/api_index.json"; + const base = window.location.pathname.split("/").slice(0, 3).join("/"); - try { - const res = await fetch(indexUrl); - const data = await res.json(); - - // EXACT match only - if (data[q] && data[q].length > 0) { - const target = data[q][0]; - const parts = target.split("."); - const mod = parts.slice(1,-1).join("/"); - window.location.href = - base + "/reference/" + mod + ".html#" + target; - return; - } - } catch (e) { - console.warn("Smart search fallback:", e); - } - - // fallback to normal RTD search + // Redirect to normal RTD search window.location.href = base + "/search.html?q=" + encodeURIComponent(q); } -document.addEventListener("DOMContentLoaded", () => { - const input = document.getElementById("smart-search-input"); - if (!input) return; - input.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - smartSearch(); - } - }); -}); document.addEventListener("DOMContentLoaded", function () { const form = document.getElementById("smartSearchForm"); const input = document.getElementById("smartSearchInput"); if (!form || !input) return; + form.addEventListener("submit", function (e) { + e.preventDefault(); + runSmartSearch(input.value); + }); + + // THIS makes Enter work input.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); - if (typeof form.requestSubmit === "function") form.requestSubmit(); - else form.dispatchEvent(new Event("submit", { cancelable: true })); + runSmartSearch(input.value); } }); }); From c04477c9aea69424295984bc913fa4ca4b61e1b9 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:10:52 -0500 Subject: [PATCH 09/23] Update conf.py --- docs/conf.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 77e7c74cf..d548caf94 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -77,9 +77,6 @@ "smart_search.js", ] -html_css_files = [ - "smart_search.css", -] # -- Extension Configuration ------------------------------------------------- # 1. Napoleon Settings (for NumPy docstrings) From 15a82f8c9e41a907821bb1372858b4b7f4728076 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:31:32 -0500 Subject: [PATCH 10/23] Update smart_search.js --- docs/_static/smart_search.js | 137 ++++++++++++++++++++++++++++++----- 1 file changed, 117 insertions(+), 20 deletions(-) diff --git a/docs/_static/smart_search.js b/docs/_static/smart_search.js index 9180dcf58..38be52695 100644 --- a/docs/_static/smart_search.js +++ b/docs/_static/smart_search.js @@ -1,29 +1,126 @@ -function runSmartSearch(query) { - const q = query.trim(); - if (!q) return; +(async function () { + const form = document.getElementById("smartSearchForm"); + const input = document.getElementById("smartSearchInput"); + const status = document.getElementById("smartSearchStatus"); - const base = window.location.pathname.split("/").slice(0, 3).join("/"); + // Create a result container (list of links) + const resultsBox = document.createElement("div"); + resultsBox.id = "smartSearchResults"; + resultsBox.style.marginTop = "12px"; + form.parentElement.appendChild(resultsBox); - // Redirect to normal RTD search - window.location.href = base + "/search.html?q=" + encodeURIComponent(q); -} + function clearResults() { + resultsBox.innerHTML = ""; + } -document.addEventListener("DOMContentLoaded", function () { - const form = document.getElementById("smartSearchForm"); - const input = document.getElementById("smartSearchInput"); + function showResults(items, query) { + clearResults(); + if (!items.length) { + resultsBox.innerHTML = `

No matches for ${escapeHtml(query)}.

`; + return; + } + + const ul = document.createElement("ul"); + ul.style.paddingLeft = "18px"; + + items.slice(0, 50).forEach((it) => { + const li = document.createElement("li"); + const a = document.createElement("a"); + a.href = it.url; + a.textContent = it.qual; + li.appendChild(a); + ul.appendChild(li); + }); + + resultsBox.appendChild(ul); + } + + function escapeHtml(s) { + return s.replace(/[&<>"']/g, (c) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[c])); + } + + async function loadIndex() { + // Works on RTD and local builds + const base = document.documentElement.dataset.contentRoot || ""; + const url = base + "_static/api_index.json"; + + const res = await fetch(url); + if (!res.ok) throw new Error("Could not load api_index.json"); + return await res.json(); // array of {name, qual, url} + } + + let index = []; + try { + index = await loadIndex(); + status.textContent = "Exact match jumps directly. Partial searches show suggestions below."; + } catch (e) { + status.textContent = "API index not found. Smart Search cannot work."; + return; + } + + // Build a fast lookup for exact matches (case-insensitive) + const byLowerName = new Map(); // lower -> [entries...] + for (const it of index) { + const key = (it.name || "").toLowerCase(); + if (!byLowerName.has(key)) byLowerName.set(key, []); + byLowerName.get(key).push(it); + } + + function doSearch(qRaw) { + const q = (qRaw || "").trim(); + clearResults(); + if (!q) return; + + const qLower = q.toLowerCase(); + + // ✅ 1) EXACT MATCH (case-insensitive): go directly to the best one + const exact = byLowerName.get(qLower); + if (exact && exact.length) { + // Prefer the canonical parsePDB location if present (optional but nice) + const preferred = exact.find(e => e.qual === "prody.proteins.pdbfile.parsePDB"); + window.location.href = (preferred || exact[0]).url; + return; + } + + // ✅ 2) Otherwise show matches here (NOT Sphinx search results) + // Rule: + // - If user typed "parse": show anything containing "parse" (case-insensitive) + // - Sort: startsWith first, then contains + const matches = index.filter(it => + (it.name || "").toLowerCase().includes(qLower) + ); + + matches.sort((a, b) => { + const an = a.name.toLowerCase(), bn = b.name.toLowerCase(); + const aStarts = an.startsWith(qLower) ? 0 : 1; + const bStarts = bn.startsWith(qLower) ? 0 : 1; + if (aStarts !== bStarts) return aStarts - bStarts; + return an.localeCompare(bn); + }); - if (!form || !input) return; + showResults(matches, q); + } - form.addEventListener("submit", function (e) { - e.preventDefault(); - runSmartSearch(input.value); + // Enter key works because we handle submit + form.addEventListener("submit", function (ev) { + ev.preventDefault(); + doSearch(input.value); }); - // THIS makes Enter work - input.addEventListener("keydown", function (e) { - if (e.key === "Enter") { - e.preventDefault(); - runSmartSearch(input.value); + // Optional: live suggestions while typing + input.addEventListener("input", function () { + const q = input.value.trim(); + if (!q) { + clearResults(); + return; } + // only show suggestions for partial queries, not for exact (exact would redirect on Enter) + doSearch(q); }); -}); +})(); From 1b31a4e784df769f0cb3fdfbef092110c4d9f577 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:32:27 -0500 Subject: [PATCH 11/23] Add files via upload --- docs/tools/build_api_index.py | 59 +++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 docs/tools/build_api_index.py diff --git a/docs/tools/build_api_index.py b/docs/tools/build_api_index.py new file mode 100644 index 000000000..fb769323c --- /dev/null +++ b/docs/tools/build_api_index.py @@ -0,0 +1,59 @@ +import json +import os +import pkgutil +import importlib +import inspect + +OUTFILE = os.path.join(os.path.dirname(__file__), "..", "_static", "api_index.json") + +def is_tests_module(modname: str) -> bool: + return modname.startswith("prody.tests") + +def module_to_html(modname: str) -> str: + # prody.proteins.pdbfile -> reference/proteins/pdbfile.html + parts = modname.split(".") + if parts[0] == "prody": + parts = parts[1:] + return "reference/" + "/".join(parts) + ".html" + +def main(): + import prody # must be importable during doc build + + # We store a list so partial search can show multiple matches + entries = [] # {name, qual, url} + + for m in pkgutil.walk_packages(prody.__path__, prefix="prody."): + modname = m.name + if is_tests_module(modname): + continue + + try: + mod = importlib.import_module(modname) + except Exception: + continue + + html = module_to_html(modname) + + for name, obj in inspect.getmembers(mod): + if name.startswith("_"): + continue + + try: + if getattr(obj, "__module__", None) != modname: + continue + except Exception: + continue + + if inspect.isfunction(obj) or inspect.isclass(obj): + qual = f"{modname}.{name}" + url = f"{html}#{qual}" + entries.append({"name": name, "qual": qual, "url": url}) + + os.makedirs(os.path.dirname(OUTFILE), exist_ok=True) + with open(OUTFILE, "w", encoding="utf-8") as f: + json.dump(entries, f, indent=2) + + print(f"Wrote {len(entries)} entries to {OUTFILE}") + +if __name__ == "__main__": + main() From dd1811c421ec6a0dcb9252ba36a0c760a7dc6ec4 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:41:45 -0500 Subject: [PATCH 12/23] Update smart_search.js --- docs/_static/smart_search.js | 233 ++++++++++++++++++++--------------- 1 file changed, 134 insertions(+), 99 deletions(-) diff --git a/docs/_static/smart_search.js b/docs/_static/smart_search.js index 38be52695..3d8045991 100644 --- a/docs/_static/smart_search.js +++ b/docs/_static/smart_search.js @@ -1,126 +1,161 @@ -(async function () { - const form = document.getElementById("smartSearchForm"); - const input = document.getElementById("smartSearchInput"); - const status = document.getElementById("smartSearchStatus"); - - // Create a result container (list of links) - const resultsBox = document.createElement("div"); - resultsBox.id = "smartSearchResults"; - resultsBox.style.marginTop = "12px"; - form.parentElement.appendChild(resultsBox); - - function clearResults() { - resultsBox.innerHTML = ""; - } - - function showResults(items, query) { - clearResults(); - if (!items.length) { - resultsBox.innerHTML = `

No matches for ${escapeHtml(query)}.

`; - return; - } - - const ul = document.createElement("ul"); - ul.style.paddingLeft = "18px"; - - items.slice(0, 50).forEach((it) => { - const li = document.createElement("li"); - const a = document.createElement("a"); - a.href = it.url; - a.textContent = it.qual; - li.appendChild(a); - ul.appendChild(li); - }); - - resultsBox.appendChild(ul); - } - +(function () { function escapeHtml(s) { - return s.replace(/[&<>"']/g, (c) => ({ + return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, - "'": "'", + "'": "'", }[c])); } + function getUrlRoot() { + // Sphinx provides this on built pages (including ReadTheDocs) + if (window.DOCUMENTATION_OPTIONS && window.DOCUMENTATION_OPTIONS.URL_ROOT) { + return window.DOCUMENTATION_OPTIONS.URL_ROOT; // e.g. "../../" on RTD + } + return ""; + } + async function loadIndex() { - // Works on RTD and local builds - const base = document.documentElement.dataset.contentRoot || ""; + const base = getUrlRoot(); const url = base + "_static/api_index.json"; - const res = await fetch(url); - if (!res.ok) throw new Error("Could not load api_index.json"); + if (!res.ok) throw new Error("Could not load api_index.json from " + url); return await res.json(); // array of {name, qual, url} } - let index = []; - try { - index = await loadIndex(); - status.textContent = "Exact match jumps directly. Partial searches show suggestions below."; - } catch (e) { - status.textContent = "API index not found. Smart Search cannot work."; - return; - } + function pickBestExact(exactList) { + // Prefer the canonical ProDy parsePDB if it exists + const preferred = exactList.find( + (e) => e.qual === "prody.proteins.pdbfile.parsePDB" + ); + if (preferred) return preferred; - // Build a fast lookup for exact matches (case-insensitive) - const byLowerName = new Map(); // lower -> [entries...] - for (const it of index) { - const key = (it.name || "").toLowerCase(); - if (!byLowerName.has(key)) byLowerName.set(key, []); - byLowerName.get(key).push(it); + // Otherwise: choose shortest qualified name (usually the most "core") + return exactList.slice().sort((a, b) => (a.qual || "").length - (b.qual || "").length)[0]; } - function doSearch(qRaw) { - const q = (qRaw || "").trim(); - clearResults(); - if (!q) return; + function buildUI() { + const form = document.getElementById("smartSearchForm"); + const input = document.getElementById("smartSearchInput"); + const status = document.getElementById("smartSearchStatus"); - const qLower = q.toLowerCase(); + const resultsBox = document.createElement("div"); + resultsBox.id = "smartSearchResults"; + resultsBox.style.marginTop = "12px"; + form.parentElement.appendChild(resultsBox); - // ✅ 1) EXACT MATCH (case-insensitive): go directly to the best one - const exact = byLowerName.get(qLower); - if (exact && exact.length) { - // Prefer the canonical parsePDB location if present (optional but nice) - const preferred = exact.find(e => e.qual === "prody.proteins.pdbfile.parsePDB"); - window.location.href = (preferred || exact[0]).url; - return; + function clearResults() { + resultsBox.innerHTML = ""; } - // ✅ 2) Otherwise show matches here (NOT Sphinx search results) - // Rule: - // - If user typed "parse": show anything containing "parse" (case-insensitive) - // - Sort: startsWith first, then contains - const matches = index.filter(it => - (it.name || "").toLowerCase().includes(qLower) - ); - - matches.sort((a, b) => { - const an = a.name.toLowerCase(), bn = b.name.toLowerCase(); - const aStarts = an.startsWith(qLower) ? 0 : 1; - const bStarts = bn.startsWith(qLower) ? 0 : 1; - if (aStarts !== bStarts) return aStarts - bStarts; - return an.localeCompare(bn); - }); + function showResults(items, query) { + clearResults(); + if (!items.length) { + resultsBox.innerHTML = `No matches for ${escapeHtml(query)}.`; + return; + } + const ul = document.createElement("ul"); + ul.style.paddingLeft = "18px"; + + items.slice(0, 50).forEach((it) => { + const li = document.createElement("li"); + const a = document.createElement("a"); + a.href = it.url; + a.textContent = it.qual; // show full path + li.appendChild(a); + ul.appendChild(li); + }); + + resultsBox.appendChild(ul); + } - showResults(matches, q); + return { form, input, status, clearResults, showResults }; } - // Enter key works because we handle submit - form.addEventListener("submit", function (ev) { - ev.preventDefault(); - doSearch(input.value); - }); - - // Optional: live suggestions while typing - input.addEventListener("input", function () { - const q = input.value.trim(); - if (!q) { - clearResults(); + (async function main() { + const ui = buildUI(); + + let index = []; + try { + index = await loadIndex(); + ui.status.textContent = + "Enter an exact function name to jump directly. Partial text shows suggestions."; + } catch (e) { + ui.status.textContent = + "API index not found (_static/api_index.json). Smart Search cannot work."; return; } - // only show suggestions for partial queries, not for exact (exact would redirect on Enter) - doSearch(q); - }); + + // Map short-name (lowercased) -> list of entries + const byLowerName = new Map(); + for (const it of index) { + const key = (it.name || "").toLowerCase(); + if (!byLowerName.has(key)) byLowerName.set(key, []); + byLowerName.get(key).push(it); + } + + function getPartialMatches(qLower) { + const matches = index.filter((it) => + (it.name || "").toLowerCase().includes(qLower) + ); + + // startsWith first, then alphabetical + matches.sort((a, b) => { + const an = (a.name || "").toLowerCase(); + const bn = (b.name || "").toLowerCase(); + const aStarts = an.startsWith(qLower) ? 0 : 1; + const bStarts = bn.startsWith(qLower) ? 0 : 1; + if (aStarts !== bStarts) return aStarts - bStarts; + return an.localeCompare(bn); + }); + + return matches; + } + + // ✅ ENTER / Search button: exact match redirects, else show suggestions + ui.form.addEventListener("submit", function (ev) { + ev.preventDefault(); + const q = (ui.input.value || "").trim(); + ui.clearResults(); + if (!q) return; + + const qLower = q.toLowerCase(); + + const exact = byLowerName.get(qLower); + if (exact && exact.length) { + const best = pickBestExact(exact); + window.location.href = best.url; + return; + } + + const matches = getPartialMatches(qLower); + ui.showResults(matches, q); + }); + + // ✅ Typing: ONLY show suggestions (do NOT redirect) + ui.input.addEventListener("input", function () { + const q = (ui.input.value || "").trim(); + ui.clearResults(); + if (!q) return; + const qLower = q.toLowerCase(); + + // If exact exists, we don't redirect while typing — we just show it first + const exact = byLowerName.get(qLower) || []; + const partial = getPartialMatches(qLower); + + // Put exact matches (if any) at top, then partial + const seen = new Set(); + const merged = []; + for (const it of exact.concat(partial)) { + const key = it.qual + "||" + it.url; + if (seen.has(key)) continue; + seen.add(key); + merged.push(it); + } + + ui.showResults(merged, q); + }); + })(); })(); From 345981b373a8cf6743aa45b03bef28e67d051ed8 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:43:36 -0500 Subject: [PATCH 13/23] Update conf.py --- docs/conf.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index d548caf94..ba6c5cac1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -103,3 +103,12 @@ 'scipy': ('https://docs.scipy.org/doc/scipy/', None), 'matplotlib': ('https://matplotlib.org/stable/', None), } +if os.environ.get("READTHEDOCS") == "True": + import subprocess + try: + subprocess.check_call( + [sys.executable, "tools/build_api_index.py"], + cwd=os.path.dirname(__file__), + ) + except Exception as e: + print("WARNING: API index generation failed:", e) From 6477c4642249fadd75072d262786428d9cffe99b Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:55:14 -0500 Subject: [PATCH 14/23] Update build_api_index.py --- docs/tools/build_api_index.py | 74 +++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/docs/tools/build_api_index.py b/docs/tools/build_api_index.py index fb769323c..8c59529f1 100644 --- a/docs/tools/build_api_index.py +++ b/docs/tools/build_api_index.py @@ -4,56 +4,64 @@ import importlib import inspect -OUTFILE = os.path.join(os.path.dirname(__file__), "..", "_static", "api_index.json") - -def is_tests_module(modname: str) -> bool: - return modname.startswith("prody.tests") - -def module_to_html(modname: str) -> str: - # prody.proteins.pdbfile -> reference/proteins/pdbfile.html - parts = modname.split(".") - if parts[0] == "prody": - parts = parts[1:] - return "reference/" + "/".join(parts) + ".html" +def iter_modules(pkg, prefix): + # walk packages but skip tests (they cause side effects / missing files on RTD) + for m in pkgutil.walk_packages(pkg.__path__, prefix): + name = m.name + if name.startswith("prody.tests"): + continue + yield name def main(): - import prody # must be importable during doc build + import prody - # We store a list so partial search can show multiple matches - entries = [] # {name, qual, url} + out_path = os.path.join(os.path.dirname(__file__), "..", "_static", "api_index.json") + out_path = os.path.abspath(out_path) - for m in pkgutil.walk_packages(prody.__path__, prefix="prody."): - modname = m.name - if is_tests_module(modname): - continue + entries = [] + seen = set() + for modname in iter_modules(prody, "prody."): try: mod = importlib.import_module(modname) except Exception: + # skip modules that fail to import on RTD continue - html = module_to_html(modname) - - for name, obj in inspect.getmembers(mod): - if name.startswith("_"): + for attr_name, obj in vars(mod).items(): + if attr_name.startswith("_"): + continue + if not inspect.isfunction(obj): + continue + if getattr(obj, "__module__", None) != modname: continue - try: - if getattr(obj, "__module__", None) != modname: - continue - except Exception: + full = f"{modname}.{attr_name}" + if full in seen: continue + seen.add(full) + + # Guess the page path used by your docs: + # prody.proteins.pdbfile.parsePDB -> reference/proteins/pdbfile.html#prody.proteins.pdbfile.parsePDB + parts = modname.split(".") + if len(parts) >= 3: + section = parts[1] + page = parts[2] + url = f"reference/{section}/{page}.html#{full}" + else: + url = f"reference/index.html#{full}" - if inspect.isfunction(obj) or inspect.isclass(obj): - qual = f"{modname}.{name}" - url = f"{html}#{qual}" - entries.append({"name": name, "qual": qual, "url": url}) + entries.append({ + "name": attr_name, + "full": full, + "url": url + }) - os.makedirs(os.path.dirname(OUTFILE), exist_ok=True) - with open(OUTFILE, "w", encoding="utf-8") as f: + os.makedirs(os.path.dirname(out_path), exist_ok=True) + with open(out_path, "w", encoding="utf-8") as f: json.dump(entries, f, indent=2) - print(f"Wrote {len(entries)} entries to {OUTFILE}") + print(f"Wrote {len(entries)} entries -> {out_path}") if __name__ == "__main__": main() From d1f644d8fdb78b592457534de78c88b0fb6e51c6 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:56:30 -0500 Subject: [PATCH 15/23] Update conf.py --- docs/conf.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ba6c5cac1..36aace251 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -103,12 +103,12 @@ 'scipy': ('https://docs.scipy.org/doc/scipy/', None), 'matplotlib': ('https://matplotlib.org/stable/', None), } -if os.environ.get("READTHEDOCS") == "True": - import subprocess - try: - subprocess.check_call( - [sys.executable, "tools/build_api_index.py"], - cwd=os.path.dirname(__file__), - ) - except Exception as e: - print("WARNING: API index generation failed:", e) +import subprocess + +def _build_api_index(app): + here = os.path.dirname(__file__) + script = os.path.join(here, "tools", "build_api_index.py") + subprocess.check_call([sys.executable, script]) + +def setup(app): + app.connect("builder-inited", _build_api_index) From b3d359e15c73a2ecd675e01a57a28c0637c45538 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:57:05 -0500 Subject: [PATCH 16/23] Update smart_search.js --- docs/_static/smart_search.js | 192 +++++++++-------------------------- 1 file changed, 50 insertions(+), 142 deletions(-) diff --git a/docs/_static/smart_search.js b/docs/_static/smart_search.js index 3d8045991..7a23fbb5f 100644 --- a/docs/_static/smart_search.js +++ b/docs/_static/smart_search.js @@ -1,161 +1,69 @@ -(function () { - function escapeHtml(s) { - return String(s).replace(/[&<>"']/g, (c) => ({ - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - }[c])); - } - - function getUrlRoot() { - // Sphinx provides this on built pages (including ReadTheDocs) - if (window.DOCUMENTATION_OPTIONS && window.DOCUMENTATION_OPTIONS.URL_ROOT) { - return window.DOCUMENTATION_OPTIONS.URL_ROOT; // e.g. "../../" on RTD - } - return ""; - } - +(async function () { async function loadIndex() { - const base = getUrlRoot(); - const url = base + "_static/api_index.json"; - const res = await fetch(url); - if (!res.ok) throw new Error("Could not load api_index.json from " + url); - return await res.json(); // array of {name, qual, url} + const r = await fetch("_static/api_index.json", { cache: "no-store" }); + if (!r.ok) throw new Error("Cannot load _static/api_index.json"); + return await r.json(); } - function pickBestExact(exactList) { - // Prefer the canonical ProDy parsePDB if it exists - const preferred = exactList.find( - (e) => e.qual === "prody.proteins.pdbfile.parsePDB" - ); - if (preferred) return preferred; - - // Otherwise: choose shortest qualified name (usually the most "core") - return exactList.slice().sort((a, b) => (a.qual || "").length - (b.qual || "").length)[0]; + function normalize(s) { + return (s || "").trim().toLowerCase(); } - function buildUI() { - const form = document.getElementById("smartSearchForm"); - const input = document.getElementById("smartSearchInput"); - const status = document.getElementById("smartSearchStatus"); - - const resultsBox = document.createElement("div"); - resultsBox.id = "smartSearchResults"; - resultsBox.style.marginTop = "12px"; - form.parentElement.appendChild(resultsBox); - - function clearResults() { - resultsBox.innerHTML = ""; + function renderResults(container, hits) { + if (!hits.length) { + container.innerHTML = "

No matches.

"; + return; } + container.innerHTML = hits + .map( + (h) => + `
+ ${h.name} +
${h.full}
+
` + ) + .join(""); + } - function showResults(items, query) { - clearResults(); - if (!items.length) { - resultsBox.innerHTML = `No matches for ${escapeHtml(query)}.`; - return; - } - const ul = document.createElement("ul"); - ul.style.paddingLeft = "18px"; - - items.slice(0, 50).forEach((it) => { - const li = document.createElement("li"); - const a = document.createElement("a"); - a.href = it.url; - a.textContent = it.qual; // show full path - li.appendChild(a); - ul.appendChild(li); - }); + let index = []; + try { + index = await loadIndex(); + } catch (e) { + const out = document.getElementById("smartSearchResults"); + if (out) out.innerHTML = `

${e.message}

`; + return; + } - resultsBox.appendChild(ul); - } + const form = document.getElementById("smartSearchForm"); + const input = document.getElementById("smartSearchInput"); + const out = document.getElementById("smartSearchResults"); - return { form, input, status, clearResults, showResults }; - } + if (!form || !input || !out) return; - (async function main() { - const ui = buildUI(); + form.addEventListener("submit", (ev) => { + ev.preventDefault(); - let index = []; - try { - index = await loadIndex(); - ui.status.textContent = - "Enter an exact function name to jump directly. Partial text shows suggestions."; - } catch (e) { - ui.status.textContent = - "API index not found (_static/api_index.json). Smart Search cannot work."; + const q = normalize(input.value); + if (!q) { + out.innerHTML = "

Type something.

"; return; } - // Map short-name (lowercased) -> list of entries - const byLowerName = new Map(); - for (const it of index) { - const key = (it.name || "").toLowerCase(); - if (!byLowerName.has(key)) byLowerName.set(key, []); - byLowerName.get(key).push(it); - } - - function getPartialMatches(qLower) { - const matches = index.filter((it) => - (it.name || "").toLowerCase().includes(qLower) - ); - - // startsWith first, then alphabetical - matches.sort((a, b) => { - const an = (a.name || "").toLowerCase(); - const bn = (b.name || "").toLowerCase(); - const aStarts = an.startsWith(qLower) ? 0 : 1; - const bStarts = bn.startsWith(qLower) ? 0 : 1; - if (aStarts !== bStarts) return aStarts - bStarts; - return an.localeCompare(bn); - }); - - return matches; + // 1) exact match on function name (case-insensitive) + const exact = index.find((x) => normalize(x.name) === q); + if (exact) { + // go directly to that function doc + window.location.href = exact.url; + return; } - // ✅ ENTER / Search button: exact match redirects, else show suggestions - ui.form.addEventListener("submit", function (ev) { - ev.preventDefault(); - const q = (ui.input.value || "").trim(); - ui.clearResults(); - if (!q) return; - - const qLower = q.toLowerCase(); - - const exact = byLowerName.get(qLower); - if (exact && exact.length) { - const best = pickBestExact(exact); - window.location.href = best.url; - return; - } - - const matches = getPartialMatches(qLower); - ui.showResults(matches, q); + // 2) otherwise show partial matches + const hits = index.filter((x) => { + const name = normalize(x.name); + const full = normalize(x.full); + return name.includes(q) || full.includes(q); }); - // ✅ Typing: ONLY show suggestions (do NOT redirect) - ui.input.addEventListener("input", function () { - const q = (ui.input.value || "").trim(); - ui.clearResults(); - if (!q) return; - const qLower = q.toLowerCase(); - - // If exact exists, we don't redirect while typing — we just show it first - const exact = byLowerName.get(qLower) || []; - const partial = getPartialMatches(qLower); - - // Put exact matches (if any) at top, then partial - const seen = new Set(); - const merged = []; - for (const it of exact.concat(partial)) { - const key = it.qual + "||" + it.url; - if (seen.has(key)) continue; - seen.add(key); - merged.push(it); - } - - ui.showResults(merged, q); - }); - })(); + renderResults(out, hits); + }); })(); From a100df9dfb206d33140fdce6cd9fd84c6ceff522 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:59:34 -0500 Subject: [PATCH 17/23] Update smart_search.rst --- docs/smart_search.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/smart_search.rst b/docs/smart_search.rst index 8d8d3b1ed..fb5a95a13 100644 --- a/docs/smart_search.rst +++ b/docs/smart_search.rst @@ -12,4 +12,4 @@ Type an exact ProDy function name (for example ``parsePDB``). - +
From 2a694b9c99daf3a29f67ef096c462a8cdfd9d6ce Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:00:38 -0500 Subject: [PATCH 18/23] Update conf.py --- docs/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 36aace251..2ee2093b6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -103,6 +103,8 @@ 'scipy': ('https://docs.scipy.org/doc/scipy/', None), 'matplotlib': ('https://matplotlib.org/stable/', None), } +import os +import sys import subprocess def _build_api_index(app): From 5dfaee2d1f2c334364c2a4a3b278b314b8f79bb2 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:04:36 -0500 Subject: [PATCH 19/23] Update build_api_index.py --- docs/tools/build_api_index.py | 105 ++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/docs/tools/build_api_index.py b/docs/tools/build_api_index.py index 8c59529f1..b3e9dbb11 100644 --- a/docs/tools/build_api_index.py +++ b/docs/tools/build_api_index.py @@ -1,67 +1,76 @@ +# -*- coding: utf-8 -*- import json import os -import pkgutil +import sys import importlib -import inspect +from pathlib import Path -def iter_modules(pkg, prefix): - # walk packages but skip tests (they cause side effects / missing files on RTD) - for m in pkgutil.walk_packages(pkg.__path__, prefix): - name = m.name - if name.startswith("prody.tests"): + +def iter_module_names(pkg_name: str, pkg_root: Path): + """ + Walk the package directory on disk WITHOUT importing subpackages. + This avoids importing prody.tests.* which breaks on RTD. + """ + for py in pkg_root.rglob("*.py"): + # Skip caches / hidden + if "__pycache__" in py.parts: + continue + + rel = py.relative_to(pkg_root) + + # Skip tests entirely (this fixes your NRAS_BRAF_GDP_model_0.cif crash) + if "tests" in rel.parts: continue - yield name + + # Build module name + parts = list(rel.parts) + if parts[-1] == "__init__.py": + parts = parts[:-1] + else: + parts[-1] = parts[-1].replace(".py", "") + + if not parts: + yield pkg_name + else: + yield pkg_name + "." + ".".join(parts) + def main(): import prody - out_path = os.path.join(os.path.dirname(__file__), "..", "_static", "api_index.json") - out_path = os.path.abspath(out_path) + pkg_name = "prody" + pkg_root = Path(prody.__file__).resolve().parent + + here = Path(__file__).resolve().parent.parent # docs/ + out_dir = here / "_static" + out_dir.mkdir(parents=True, exist_ok=True) + out_file = out_dir / "api_index.json" - entries = [] - seen = set() + results = [] - for modname in iter_modules(prody, "prody."): + for modname in sorted(set(iter_module_names(pkg_name, pkg_root))): try: mod = importlib.import_module(modname) except Exception: - # skip modules that fail to import on RTD + # Some modules may require optional deps or side effects. + # We skip them so docs build doesn't fail. continue - for attr_name, obj in vars(mod).items(): - if attr_name.startswith("_"): - continue - if not inspect.isfunction(obj): - continue - if getattr(obj, "__module__", None) != modname: - continue - - full = f"{modname}.{attr_name}" - if full in seen: - continue - seen.add(full) - - # Guess the page path used by your docs: - # prody.proteins.pdbfile.parsePDB -> reference/proteins/pdbfile.html#prody.proteins.pdbfile.parsePDB - parts = modname.split(".") - if len(parts) >= 3: - section = parts[1] - page = parts[2] - url = f"reference/{section}/{page}.html#{full}" - else: - url = f"reference/index.html#{full}" - - entries.append({ - "name": attr_name, - "full": full, - "url": url - }) - - os.makedirs(os.path.dirname(out_path), exist_ok=True) - with open(out_path, "w", encoding="utf-8") as f: - json.dump(entries, f, indent=2) - - print(f"Wrote {len(entries)} entries -> {out_path}") + for name, obj in vars(mod).items(): + if callable(obj) and getattr(obj, "__module__", "") == modname: + qual = f"{modname}.{name}" + results.append( + { + "name": name, # parsePDB + "qualname": qual, # prody.proteins.pdbfile.parsePDB + } + ) + + with out_file.open("w", encoding="utf-8") as f: + json.dump(results, f, indent=2) + + print(f"[smart_search] wrote {len(results)} entries to {out_file}") + if __name__ == "__main__": main() From 8c98ea41497b017efaf9394c1ae7cca252041276 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:05:17 -0500 Subject: [PATCH 20/23] Update conf.py --- docs/conf.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 2ee2093b6..2cd3f8220 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -110,7 +110,12 @@ def _build_api_index(app): here = os.path.dirname(__file__) script = os.path.join(here, "tools", "build_api_index.py") - subprocess.check_call([sys.executable, script]) + + try: + subprocess.check_call([sys.executable, script]) + except Exception as e: + # IMPORTANT: don't fail the docs build + print("WARNING: Smart Search API index generation failed:", repr(e)) def setup(app): app.connect("builder-inited", _build_api_index) From d691fff65212c68c3dbb9956bb43351feade115e Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:19:59 -0500 Subject: [PATCH 21/23] Update conf.py --- docs/conf.py | 60 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2cd3f8220..ece31e375 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -105,17 +105,57 @@ } import os import sys -import subprocess +import json +import os +from sphinx.util.inventory import InventoryFile + +def build_api_index(app, exception): + # If build failed, don't do anything + if exception is not None: + return + + inv_path = os.path.join(app.outdir, "objects.inv") + if not os.path.isfile(inv_path): + print("WARNING: objects.inv not found, skipping api_index.json generation") + return + + with open(inv_path, "rb") as f: + inv = InventoryFile.load(f, "", lambda base, uri: uri) + + # Map lowercase simple name -> URL + # Also keep full names in case you want them later + exact_simple = {} + exact_full = {} + + # Prefer functions over methods if there's a collision + role_priority = ["py:function", "py:method"] + + for role in role_priority: + if role not in inv: + continue + for fullname, data in inv[role].items(): + # data = (project, version, uri, dispname) + uri = data[2] + # Sphinx inventory sometimes uses "$" as placeholder for fullname + uri = uri.replace("$", fullname) + + simple = fullname.split(".")[-1].lower() + full_lower = fullname.lower() + + # fill full map always + exact_full[full_lower] = uri + + # fill simple map only if empty (function wins because we iterate functions first) + if simple not in exact_simple: + exact_simple[simple] = uri + + out_path = os.path.join(app.outdir, "_static", "api_index.json") + os.makedirs(os.path.dirname(out_path), exist_ok=True) -def _build_api_index(app): - here = os.path.dirname(__file__) - script = os.path.join(here, "tools", "build_api_index.py") + with open(out_path, "w", encoding="utf-8") as f: + json.dump({"exact_simple": exact_simple, "exact_full": exact_full}, f) - try: - subprocess.check_call([sys.executable, script]) - except Exception as e: - # IMPORTANT: don't fail the docs build - print("WARNING: Smart Search API index generation failed:", repr(e)) + print(f"Wrote {out_path} with {len(exact_simple)} simple entries") def setup(app): - app.connect("builder-inited", _build_api_index) + app.connect("build-finished", build_api_index) From 8bde7aea5d38f6ab93efae1c6426954b80f35e70 Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:21:00 -0500 Subject: [PATCH 22/23] Update smart_search.js --- docs/_static/smart_search.js | 103 +++++++++++++++++------------------ 1 file changed, 49 insertions(+), 54 deletions(-) diff --git a/docs/_static/smart_search.js b/docs/_static/smart_search.js index 7a23fbb5f..d0e442fc8 100644 --- a/docs/_static/smart_search.js +++ b/docs/_static/smart_search.js @@ -1,69 +1,64 @@ -(async function () { +document.addEventListener("DOMContentLoaded", () => { + const form = document.getElementById("smartSearchForm"); + const input = document.getElementById("smartSearchInput"); + const status = document.getElementById("smartSearchStatus"); + + if (!form || !input) return; + + const URL_ROOT = + (window.DOCUMENTATION_OPTIONS && window.DOCUMENTATION_OPTIONS.URL_ROOT) || "./"; + + const apiIndexUrl = URL_ROOT + "_static/api_index.json"; + async function loadIndex() { - const r = await fetch("_static/api_index.json", { cache: "no-store" }); - if (!r.ok) throw new Error("Cannot load _static/api_index.json"); + const r = await fetch(apiIndexUrl, { cache: "no-store" }); + if (!r.ok) throw new Error(`Failed to load api_index.json (${r.status})`); return await r.json(); } - function normalize(s) { - return (s || "").trim().toLowerCase(); + function goToSearch(query) { + window.location.href = URL_ROOT + "search.html?q=" + encodeURIComponent(query); } - function renderResults(container, hits) { - if (!hits.length) { - container.innerHTML = "

No matches.

"; - return; - } - container.innerHTML = hits - .map( - (h) => - `
- ${h.name} -
${h.full}
-
` - ) - .join(""); - } + form.addEventListener("submit", async (e) => { + e.preventDefault(); - let index = []; - try { - index = await loadIndex(); - } catch (e) { - const out = document.getElementById("smartSearchResults"); - if (out) out.innerHTML = `

${e.message}

`; - return; - } + const q = (input.value || "").trim(); + if (!q) return; - const form = document.getElementById("smartSearchForm"); - const input = document.getElementById("smartSearchInput"); - const out = document.getElementById("smartSearchResults"); + if (status) status.textContent = "Searching..."; - if (!form || !input || !out) return; + try { + const idx = await loadIndex(); - form.addEventListener("submit", (ev) => { - ev.preventDefault(); + const key = q.toLowerCase(); - const q = normalize(input.value); - if (!q) { - out.innerHTML = "

Type something.

"; - return; - } + // 1) Exact match by simple name (parsepdb -> parsePDB) + if (idx.exact_simple && idx.exact_simple[key]) { + window.location.href = URL_ROOT + idx.exact_simple[key]; + return; + } - // 1) exact match on function name (case-insensitive) - const exact = index.find((x) => normalize(x.name) === q); - if (exact) { - // go directly to that function doc - window.location.href = exact.url; - return; - } + // 2) Exact match by full name if user pasted it + if (idx.exact_full && idx.exact_full[key]) { + window.location.href = URL_ROOT + idx.exact_full[key]; + return; + } - // 2) otherwise show partial matches - const hits = index.filter((x) => { - const name = normalize(x.name); - const full = normalize(x.full); - return name.includes(q) || full.includes(q); - }); + // 3) No exact match -> normal Sphinx search (prefix search) + goToSearch(q); + } catch (err) { + console.error(err); + if (status) status.textContent = "Index not loaded; using normal search."; + goToSearch(q); + } + }); - renderResults(out, hits); + // Optional: make Enter always submit even if browser is weird + input.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + form.requestSubmit(); + } }); -})(); +}); From 1e840a0ae83873822a08f4a93e800d83df127e5f Mon Sep 17 00:00:00 2001 From: Satyaki Saha <85615620+sahags@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:24:24 -0500 Subject: [PATCH 23/23] Update smart_search.rst --- docs/smart_search.rst | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/smart_search.rst b/docs/smart_search.rst index fb5a95a13..7ac8e5042 100644 --- a/docs/smart_search.rst +++ b/docs/smart_search.rst @@ -1,15 +1,19 @@ Smart Search ============ -Type an exact ProDy function name (for example ``parsePDB``). +Type a ProDy function name. -.. raw:: html +- If it's an exact match (case-insensitive), you go directly to that function page. +- Otherwise, it falls back to normal search. -
- - -
+.. raw:: html -
+
+
+ + +
+

+