"""HTML and JavaScript injection for sphinx-typesense search UI.
This module handles injecting the search configuration and container
into Sphinx HTML pages, with support for multiple backends (Typesense
and Pagefind).
The injection process:
1. Determines the effective backend from configuration
2. Adds search container div to page context
3. Injects backend-specific JavaScript configuration
4. Handles theme-specific search bar placement
Assets:
Typesense backend:
- typesense-docsearch.css: DocSearch styling
- typesense-docsearch.js: DocSearch library (from CDN)
- typesense-init.js: Initialization script with config
Pagefind backend:
- pagefind-ui.css: Pagefind UI styling
- pagefind-init.js: Initialization script with config
Example:
The module is used automatically via Sphinx event::
app.connect("html-page-context", inject_search_assets)
"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING, Any
from sphinx.util import logging
from sphinx_typesense.config import get_effective_backend
if TYPE_CHECKING:
from sphinx.application import Sphinx
logger = logging.getLogger(__name__)
[docs]
def inject_search_assets(
app: Sphinx,
pagename: str,
templatename: str, # noqa: ARG001
context: dict[str, Any],
doctree: Any, # noqa: ARG001
) -> None:
"""Inject search configuration into page context based on selected backend.
This function is connected to Sphinx's html-page-context event
and adds the search UI HTML and JavaScript configuration to
each page. The configuration varies based on the selected backend
(Typesense or Pagefind).
Args:
app: The Sphinx application instance.
pagename: Name of the current page being rendered.
templatename: Name of the template being used.
context: Template context dictionary to modify.
doctree: The document tree (may be None for some pages).
Note:
The search container and configuration are added to the
context as 'typesense_search_html' which can be included
in templates or injected via theme-specific methods.
"""
logger.debug("sphinx-typesense: Injecting search assets for page: %s", pagename)
# Get the effective backend
backend_name = get_effective_backend(app.config)
logger.debug("sphinx-typesense: Using backend: %s", backend_name)
# Generate the search container HTML
container_html = get_search_container_html(app)
logger.debug("sphinx-typesense: Generated search container HTML")
# Generate the config script based on backend
config_script = get_typesense_config_script(app) if backend_name == "typesense" else get_pagefind_config_script(app)
logger.debug("sphinx-typesense: Generated config script for %s backend", backend_name)
# Combine into a single HTML block
search_html = f"{container_html}\n{config_script}"
# Add to template context
context["typesense_search_html"] = search_html
logger.debug("sphinx-typesense: Added typesense_search_html to context")
logger.debug("sphinx-typesense: Search assets injection complete for page: %s", pagename)
[docs]
def get_search_container_html(app: Sphinx) -> str:
"""Generate the search container HTML.
Args:
app: The Sphinx application instance.
Returns:
HTML string for the search container div.
"""
container = app.config.typesense_container
# Strip leading # if present to get the ID
container_id = container.lstrip("#") if container.startswith("#") else container
logger.debug("sphinx-typesense: Using container ID: %s", container_id)
return f'<div id="{container_id}"></div>'
[docs]
def get_config_script(app: Sphinx) -> str:
"""Generate the JavaScript configuration script (deprecated).
This function is kept for backward compatibility. New code should
use get_typesense_config_script() or get_pagefind_config_script()
based on the selected backend.
Args:
app: The Sphinx application instance.
Returns:
JavaScript script tag with TYPESENSE_CONFIG object.
"""
return get_typesense_config_script(app)
[docs]
def get_typesense_config_script(app: Sphinx) -> str:
"""Generate JavaScript configuration for Typesense DocSearch.
Creates a script tag containing the TYPESENSE_CONFIG object with
all necessary configuration for the DocSearch frontend to connect
to the Typesense server.
Args:
app: The Sphinx application instance.
Returns:
JavaScript script tag with window.TYPESENSE_CONFIG object.
Note:
Only the search API key is exposed in the frontend configuration,
never the admin API key.
"""
logger.debug("sphinx-typesense: Building Typesense JavaScript configuration")
# Build configuration object
config = {
"collectionName": app.config.typesense_collection_name,
"host": app.config.typesense_host,
"port": str(app.config.typesense_port),
"protocol": app.config.typesense_protocol,
"apiKey": app.config.typesense_search_api_key, # Use search-only key, NOT admin key
"placeholder": app.config.typesense_placeholder,
"numTypos": app.config.typesense_num_typos,
"perPage": app.config.typesense_per_page,
"filterBy": app.config.typesense_filter_by,
"container": app.config.typesense_container,
}
logger.debug(
"sphinx-typesense: Config script - collection=%s, host=%s, port=%s",
config["collectionName"],
config["host"],
config["port"],
)
# Warn if search API key is missing
if not config["apiKey"]:
logger.warning("sphinx-typesense: Search API key is not configured - search may not work")
# Use json.dumps for proper escaping and formatting
config_json = json.dumps(config, indent=2)
return f"""<script>
window.TYPESENSE_CONFIG = {config_json};
</script>"""
[docs]
def get_pagefind_config_script(app: Sphinx) -> str:
"""Generate JavaScript configuration for Pagefind UI.
Creates a script tag containing the PAGEFIND_CONFIG object with
configuration for the Pagefind UI frontend.
Args:
app: The Sphinx application instance.
Returns:
JavaScript script tag with window.PAGEFIND_CONFIG object.
"""
logger.debug("sphinx-typesense: Building Pagefind JavaScript configuration")
config = {
"container": app.config.typesense_container,
"placeholder": app.config.typesense_placeholder,
"basePath": "/_pagefind/",
}
logger.debug(
"sphinx-typesense: Pagefind config - container=%s, basePath=%s",
config["container"],
config["basePath"],
)
config_json = json.dumps(config, indent=2)
return f"""<script>
window.PAGEFIND_CONFIG = {config_json};
</script>"""