Source code for sphinx_searchlite._index

"""Build the client-side search index.

Sphinx's own ``searchindex.js`` is coupled to ``searchtools.js`` and its
dedicated ``search.html`` page. This emits plain records any front end can
score itself, keyed so that section anchors survive incremental builds.
"""

import json
import re
from pathlib import Path

from docutils import nodes

__all__ = ("collect_records", "setup_index")

_WHITESPACE = re.compile(r"\s+")
_ATTRIBUTE = "_sphinx_searchlite_records"


def _flatten(text: str, limit: int) -> str:
    return _WHITESPACE.sub(" ", text).strip()[:limit]


def _own_text(section: nodes.Element, limit: int) -> str:
    """Text belonging to a section, excluding its title and nested sections."""
    parts = [child.astext() for child in section.children if not isinstance(child, (nodes.section, nodes.title))]
    return _flatten(" ".join(parts), limit)


[docs] def collect_records(app, pagename: str, doctree: nodes.document) -> list[dict[str, str]]: """Return one record for the page plus one per addressable section. Keys are short because the index ships to the browser: ``u`` url, ``t`` page title, ``s`` section heading, ``x`` text. """ limit = app.config.searchlite_max_text uri = app.builder.get_target_uri(pagename) title_node = app.env.titles.get(pagename) page_title = title_node.astext() if title_node else pagename records = [{"u": uri, "t": page_title, "x": _flatten(doctree.astext(), limit)}] for section in doctree.findall(nodes.section): if not section["ids"]: continue title = section.next_node(nodes.title) if title is None: continue heading = title.astext() if heading == page_title and section.parent is doctree: continue records.append({"u": f"{uri}#{section['ids'][0]}", "t": page_title, "s": heading, "x": _own_text(section, limit)}) return records
def _store(app) -> dict[str, list[dict[str, str]]]: # Keyed by docname so incremental builds keep records for unchanged pages. if not hasattr(app.env, _ATTRIBUTE): setattr(app.env, _ATTRIBUTE, {}) return getattr(app.env, _ATTRIBUTE) def _on_page_context(app, pagename, templatename, context, doctree): if doctree is None or app.builder.format != "html": return _store(app)[pagename] = collect_records(app, pagename, doctree) def _on_build_finished(app, exception): if exception is not None or app.builder.format != "html": return found = app.env.found_docs records = [record for docname, entries in sorted(_store(app).items()) if docname in found for record in entries] static = Path(app.builder.outdir) / "_static" static.mkdir(parents=True, exist_ok=True) (static / app.config.searchlite_index_filename).write_text(json.dumps(records, separators=(",", ":")), encoding="utf-8")
[docs] def setup_index(app) -> None: app.connect("html-page-context", _on_page_context) app.connect("build-finished", _on_build_finished)