Metadata-Version: 2.4
Name: sphinx-riddle-whisper
Version: 1.3.0
Summary: Sphinx extension that shows full glossary term definitions in a popup (no truncation, offline, vanilla JS).
Author: usaturn
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Framework :: Sphinx :: Extension
Requires-Dist: sphinx>=9
Requires-Dist: nh3
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/usaturn/sphinx-riddle-whisper
Description-Content-Type: text/x-rst

=====================
sphinx-riddle-whisper
=====================

.. |pypi| image:: https://img.shields.io/pypi/v/sphinx-riddle-whisper
   :target: https://pypi.org/project/sphinx-riddle-whisper/
   :alt: PyPI

.. |python| image:: https://img.shields.io/pypi/pyversions/sphinx-riddle-whisper
   :target: https://pypi.org/project/sphinx-riddle-whisper/
   :alt: Python versions

.. |license| image:: https://img.shields.io/badge/License-MIT-yellow.svg
   :target: https://opensource.org/licenses/MIT
   :alt: License: MIT

.. |lang-ja| image:: https://img.shields.io/badge/lang-%E6%97%A5%E6%9C%AC%E8%AA%9E-red
   :target: README.ja.rst
   :alt: 日本語

|pypi| |python| |license| |lang-ja|

A Sphinx extension that shows full glossary term definitions in a popup — no
truncation, offline, vanilla JS.

When a reader hovers or clicks a ``:term:`` reference, the complete glossary
definition appears in a popover right where they are reading, so they never have
to jump to the glossary page and back.

Features
========

- **No truncation** — the full glossary definition is shown, with a configurable
  max height/width and scrolling for long entries.
- **Offline** — definitions are rendered at build time and injected as inline
  ``<template>`` elements. No network requests, no CDN.
- **Vanilla JS** — no third-party runtime libraries are bundled.
- **Hover and click** — open popovers on hover, click, or both.
- **Footnotes & citations** — optionally show footnote and citation bodies in the
  same popover style.
- **Image lightbox** — optionally open linked images in a focus-trapped lightbox.
- **Layered sanitization** — HTML is sanitized with `nh3 <https://pypi.org/project/nh3/>`_
  at build time and re-checked in the browser as a second line of defense.
- **Incremental build aware** — pages that reference a term are rebuilt when the
  term's home definition changes.
- **Nested popovers** — a ``:term:`` link inside a popover opens a second-level
  popover (capped at two levels, configurable).

Requirements
============

- Python >= 3.11
- Sphinx >= 9

Installation
============

From PyPI:

.. code-block:: bash

   pip install sphinx-riddle-whisper

From source:

.. code-block:: bash

   pip install git+https://github.com/usaturn/sphinx-riddle-whisper.git

Or clone and install locally (this project uses `uv <https://docs.astral.sh/uv/>`_):

.. code-block:: bash

   git clone https://github.com/usaturn/sphinx-riddle-whisper.git
   cd sphinx-riddle-whisper
   uv pip install .

Quick Start
===========

Add the extension to your ``conf.py``:

.. code-block:: python

   extensions = ["sphinx_riddle_whisper"]

Define your terms with the standard ``glossary`` directive:

.. code-block:: rst

   .. glossary::

      reStructuredText
          A plain-text markup syntax and parser system, and the default
          markup language used by Sphinx.

Then reference them from any page with the ``:term:`` role:

.. code-block:: rst

   Sphinx documents are usually written in :term:`reStructuredText`.

Build your HTML as usual. Hovering or clicking the ``:term:`` reference now opens
a popover containing the full definition — no extra markup required.

Configuration
=============

All options are set in ``conf.py``. The display and feature options and their
defaults are:

.. list-table::
   :header-rows: 1
   :widths: 28 12 22 38

   * - Option
     - Type
     - Default
     - Description
   * - ``riddle_trigger``
     - str
     - ``"both"``
     - How popovers open: ``"hover"``, ``"click"``, or ``"both"``.
   * - ``riddle_max_height``
     - str
     - ``"24rem"``
     - Maximum popover height (any CSS length).
   * - ``riddle_max_width``
     - str
     - ``"32rem"``
     - Maximum popover width (any CSS length).
   * - ``riddle_open_delay_ms``
     - int
     - ``150``
     - Delay before opening on hover, in milliseconds (>= 0).
   * - ``riddle_close_delay_ms``
     - int
     - ``100``
     - Delay before closing on hover-out, in milliseconds (>= 0).
   * - ``riddle_interactive``
     - bool
     - ``True``
     - Keep the popover open while the pointer is over it.
   * - ``riddle_include_term_title``
     - bool
     - ``True``
     - Show the term name as a heading at the top of the popover.
   * - ``riddle_footnotes``
     - bool
     - ``True``
     - Enable popovers for footnote and citation references.
   * - ``riddle_image_popup``
     - bool
     - ``True``
     - Enable the image lightbox.
   * - ``riddle_nested``
     - bool
     - ``True``
     - Open a second, nested popover from ``:term:`` links inside a popover.
   * - ``riddle_mark_terms``
     - bool
     - ``True``
     - Visually mark ``:term:`` links that pop up (dotted underline + help cursor).
   * - ``riddle_table_align``
     - str
     - ``"left"``
     - Alignment of tables inside popovers: ``"left"``, ``"center"``, or ``"right"``.

Example showing the defaults:

.. code-block:: python

   riddle_trigger = "both"
   riddle_max_height = "24rem"
   riddle_max_width = "32rem"
   riddle_open_delay_ms = 150
   riddle_close_delay_ms = 100
   riddle_interactive = True
   riddle_include_term_title = True
   riddle_footnotes = True
   riddle_image_popup = True
   riddle_nested = True
   riddle_mark_terms = True
   riddle_table_align = "left"

Footnotes, Citations & Image Lightbox
=====================================

With ``riddle_footnotes = True`` (the default), footnote and citation references
open a popover containing the footnote or citation body, drawn from the same page
— so readers can check a reference without losing their place.

With ``riddle_image_popup = True`` (the default), a linked image opens in a
focus-trapped, scroll-locked lightbox. Only safe image URLs (recognized image
extensions over allowed schemes) are opened.

Set either option to ``False`` to disable that feature.

Nested Popovers
===============

With ``riddle_nested = True`` (the default), a ``:term:`` link inside an open
popover opens a second, nested popover on top of the first, so definitions that
reference other terms can be explored without leaving the page. Nesting is
capped at two levels — ``:term:`` links inside the second popover behave as
normal links — and a link pointing to the term already shown in the first
popover does not open a duplicate. Press Esc to close the inner popover first,
then the outer one. Set the option to ``False`` to keep popover-internal
``:term:`` links inert.

Visual marking of term links
============================

With ``riddle_mark_terms = True`` (default), only ``:term:`` links that
actually show a popover get a dotted underline and a ``help`` cursor, so
readers can tell them apart from ordinary links. Links whose definition is
not injected into the page, or environments without JavaScript, show no
marking (the link color is left to the theme). Note that ``:term:`` links
rendered inside a popover are not marked.

The underline can be customized via CSS variables:

.. code-block:: css

   :root {
     --riddle-term-underline-style: dashed;  /* default: dotted */
     --riddle-term-underline-color: #0a7;    /* default: currentColor */
   }

Table Alignment Inside Popovers
===============================

Sphinx renders tables without an explicit ``:align:`` option with the
``align-default`` class, which most themes center. Inside the narrow popover
this looks odd, so such tables are left-aligned by default. Set
``riddle_table_align`` to ``"center"`` or ``"right"`` to change this. Tables
with an explicit ``:align:`` option keep their author-specified alignment,
and the setting also applies to tables inside footnote and citation popovers.

Security
========

Glossary definitions can contain arbitrary inline HTML, so the extension
sanitizes content in two layers:

- **Build time** — definition fragments are sanitized with
  `nh3 <https://pypi.org/project/nh3/>`_ against a built-in allowlist of tags,
  attributes, and URL schemes before being injected.
- **Runtime** — before a popover is shown, the cloned fragment is walked again in
  the browser: elements outside the allowlist are removed, ``on*`` handlers and
  dangerous URL schemes are stripped, and ``target="_blank"`` links get
  ``rel="noopener noreferrer"``.

The safety-related options let you tune or replace the build-time allowlist:

.. list-table::
   :header-rows: 1
   :widths: 30 18 18 34

   * - Option
     - Type
     - Default
     - Description
   * - ``riddle_sanitize``
     - bool
     - ``True``
     - Enable build-time HTML sanitization with nh3.
   * - ``riddle_allowed_tags``
     - tuple[str] | None
     - ``None``
     - Allowed HTML tags. ``None`` uses the built-in allowlist.
   * - ``riddle_allowed_attributes``
     - dict[str, tuple[str]] | None
     - ``None``
     - Allowed attributes per tag. ``None`` uses the built-in allowlist.
   * - ``riddle_allowed_schemes``
     - tuple[str] | None
     - ``None``
     - Allowed URL schemes. ``None`` uses the built-in allowlist.
   * - ``riddle_strip_classes``
     - tuple[str]
     - ``("headerlink", "sd-stretched-link")``
     - CSS class names whose anchors are removed from the definition.

Documentation
=============

The full documentation is built with Sphinx from the ``docs/`` directory of this
repository.

License
=======

MIT
