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
64 changes: 64 additions & 0 deletions docs/_static/smart_search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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(apiIndexUrl, { cache: "no-store" });
if (!r.ok) throw new Error(`Failed to load api_index.json (${r.status})`);
return await r.json();
}

function goToSearch(query) {
window.location.href = URL_ROOT + "search.html?q=" + encodeURIComponent(query);
}

form.addEventListener("submit", async (e) => {
e.preventDefault();

const q = (input.value || "").trim();
if (!q) return;

if (status) status.textContent = "Searching...";

try {
const idx = await loadIndex();

const key = q.toLowerCase();

// 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;
}

// 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;
}

// 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);
}
});

// Optional: make Enter always submit even if browser is weird
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
form.requestSubmit();
}
});
});
59 changes: 59 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@

# Ensure this is set so Sphinx looks in the _static folder
html_static_path = ['_static']
html_js_files = [
"smart_search.js",
]

# -- Extension Configuration -------------------------------------------------

Expand Down Expand Up @@ -100,3 +103,59 @@
'scipy': ('https://docs.scipy.org/doc/scipy/', None),
'matplotlib': ('https://matplotlib.org/stable/', None),
}
import os
import sys
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)

with open(out_path, "w", encoding="utf-8") as f:
json.dump({"exact_simple": exact_simple, "exact_full": exact_full}, f)

print(f"Wrote {out_path} with {len(exact_simple)} simple entries")

def setup(app):
app.connect("build-finished", build_api_index)
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ ProDy Manual

.. toctree::
:maxdepth: 2
:glob:

smart_search
getprody
apps/index
reference/index
Expand Down
19 changes: 19 additions & 0 deletions docs/smart_search.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Smart Search
============

Type a ProDy function name.

- If it's an exact match (case-insensitive), you go directly to that function page.
- Otherwise, it falls back to normal search.

.. raw:: html

<div style="max-width: 700px;">
<form id="smartSearchForm">
<input id="smartSearchInput"
placeholder="Enter function name (e.g. parsePDB)"
style="width:100%; padding:10px; font-size:16px;" />
<button type="submit" style="margin-top:10px; padding:8px 12px;">Search</button>
</form>
<p id="smartSearchStatus" style="margin-top:10px; opacity:0.8;"></p>
</div>
76 changes: 76 additions & 0 deletions docs/tools/build_api_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
import json
import os
import sys
import importlib
from pathlib import Path


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

# 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

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"

results = []

for modname in sorted(set(iter_module_names(pkg_name, pkg_root))):
try:
mod = importlib.import_module(modname)
except Exception:
# Some modules may require optional deps or side effects.
# We skip them so docs build doesn't fail.
continue

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()
Loading