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
11 changes: 10 additions & 1 deletion doc/api/python/extensions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Re-reads a document when its sphinx-needs TOML file or the needs it imports chan
----------------

The type colors, the status and verdict classes and the previews of the zdocs
need cards.
need cards, and the card in a PDF.

.. automodule:: need_cards
:members:
Expand All @@ -100,6 +100,15 @@ its text.
.. automodule:: pdf_links
:members:

``pdf_tables``
----------------

Gives a need table in a PDF column widths from its content, and lets it break
across pages.

.. automodule:: pdf_tables
:members:

``xref_builder``
------------------

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
0014. The PDF layout is a LaTeX style file
==========================================

Status
------

Accepted, 2026-10-14.

Context
-------

The look of a zdocs PDF was the Sphinx default with a few changes: strings
of LaTeX in ``zdocs_conf.py`` for the running header and footer. The result
had several problems:

- The body text used a serif font, and the headings and the title page used
a sans-serif font.
- The contents were on a page of their own, and the title page was mostly
empty.
- A need card was a table of grey rows, because the card look is CSS, which
does not reach LaTeX. The fields came before the text, and their labels
looked like their values.
- A need table did not fit the page.

A consumer could change the PDF only by replacing ``latex_elements`` in its
``conf.py``, which also drops the engine's settings.

Decision
--------

- The engine has a LaTeX package, ``zdocs.sty`` (``sphinx/_latex/``). Each
PDF loads it after Sphinx's own package. ``zdocs_conf`` gives it only the
values of the build, with ``\zdsetup{...}``.
- ``zdocs.sty`` changes the Sphinx layout only through what Sphinx
documents: the page styles ``normal`` and ``plain``, ``\sphinxmaketitle``,
``\sphinxtableofcontents``, the table hooks ``\sphinxattablestart`` and
``\sphinxatlongtablestart``, the class environments ``sphinxclass<name>``
and the paragraph start ``\sphinxAtStartPar``. It does not copy or patch
``sphinx.sty``.
- ``zdocs.sty`` loads no package that a Sphinx xelatex build does not load
already. The need card uses ``framed``, not ``tcolorbox``.
- An extension writes semantic markup where a style needs it: ``need_cards``
writes a zdocs card as ``zdneed`` with ``\zdneedhead``, ``\zdneedtitle``
and ``\zdneedfield``; ``pdf_tables`` gives a need table its widths and its
class. The style file decides how this markup looks, as the CSS does for
the HTML.
- A consumer gives its own style file with ``configure(latex_style=...)``.
The PDF loads it last, so it can set the keys, colors, fonts and macros
again (:doc:`../../howto/change-the-look-of-the-pdf`).

Consequences
------------

- The PDF layout is in one file, in LaTeX, and not in Python strings.
- A Sphinx upgrade can change a documented hook. Sphinx is pinned
(``requirements-doc.txt``), so an upgrade is a deliberate step, and the
acceptance suite builds a PDF.
- The look of a card is in two places: ``zdocs-needs.css`` for HTML and
``zdocs.sty`` for the PDF. The accent of a type and the colors of a status
are the same in both; a change to one needs the same change to the other.
- A consumer that defines a macro of ``zdocs.sty`` again does not get the
changes of a new zdocs version for that macro.
- The default fonts stay the ones that Sphinx needs (FreeSans, FreeMono). A
consumer that wants other fonts must install them on each machine that
builds the PDF.
1 change: 1 addition & 0 deletions doc/manual/explanation/decisions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ repeating it.
0011-documentation-structure
0012-relative-cross-document-links
0013-linked-pdf-set
0014-pdf-layout-in-a-style-file
98 changes: 98 additions & 0 deletions doc/manual/howto/change-the-look-of-the-pdf.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
Change the look of the PDF
==========================

The look of a zdocs PDF is in the LaTeX package ``zdocs.sty``
(``sphinx/_latex/`` in the engine). Each PDF loads it after Sphinx's own
package. To change the look for your documents, give a style file of your
own. The PDF loads it after ``zdocs.sty``, so it can change what
``zdocs.sty`` sets.

Give a style file
-----------------

1. Put a ``.sty`` file in the folder of the document, for example
``doc/handbook/acme-pdf.sty``. Do not call it ``zdocs.sty``.
2. Give its path to ``configure()`` in the document's ``conf.py``:

.. code-block:: python

from pathlib import Path

from zdocs_conf import configure

configure(
globals(),
Path(__file__).parent,
project="ACME Handbook",
latex_style=Path(__file__).parent / "acme-pdf.sty",
)

Use an absolute path: Sphinx reads ``conf.py`` from a copy of the
document folder in the build tree.
3. Build the PDF (``<document>-latex``).

What a style file can change
----------------------------

Settings, with ``\zdsetup{key=value, ...}``. ``zdocs_conf`` sets these from
the build; a style file can set them again:

``title``
The running header, left. Default: the document title.
``version``
The running header, right, and the title page. Default: the document
version.
``footer``
The running footer, left, and the title page. Default:
``Copyright <year> <holder>``.
``docid``
The document ID on the title page. Default: the registry ID.

Colors, with ``\definecolor`` (xcolor):

``zdRule``, ``zdMuted``
The lines and the text of the running header and footer.
``zdLabel``
The labels of the fields in a need card, and of the rows on the title page.
``zdTonegood``, ``zdTonebad``, ``zdTonegap``, ``zdTonenone``
The status badge of a need card: passed, failed, partial, not run. A
status of another value (for example ``approved``) has the accent color of
its need type.

Fonts, with fontspec. The text and the headings use the sans-serif font,
the code the monospaced font:

.. code-block:: latex

\setsansfont{TeX Gyre Heros}
\setmonofont{TeX Gyre Cursor}

The font must be in the TeX installation of each machine that builds the
PDF, also in CI. The default fonts (FreeSans, FreeMono) are the ones that a
Sphinx PDF needs already.

The width of the label column of a need card:

.. code-block:: latex

\setlength{\zdneedlabelwidth}{9em}

For a bigger change, a style file can define a macro of ``zdocs.sty`` again
(``\renewcommand``, ``\renewenvironment``): ``\sphinxmaketitle`` (the title
page), the environment ``zdneed`` and the macros ``\zdneedhead``,
``\zdneedtitle`` and ``\zdneedfield`` (the need card). Their arguments are in
the comments of ``zdocs.sty``. A macro that you define again does not get
the changes of a new zdocs version.

An example
----------

.. code-block:: latex

% acme-pdf.sty
\ProvidesPackage{acme-pdf}
\zdsetup{footer={ACME Instruments Ltd. -- Confidential}}
\definecolor{zdRule}{HTML}{005A9C}
\setlength{\zdneedlabelwidth}{8em}

See also :doc:`../explanation/decisions/0014-pdf-layout-in-a-style-file`.
1 change: 1 addition & 0 deletions doc/manual/howto/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ assume you already have a docset and know your way around it.
build-these-docs
add-a-document
publish-a-deploy-tree
change-the-look-of-the-pdf
render-test-specifications
run-the-test-suites
11 changes: 9 additions & 2 deletions doc/manual/reference/directives-and-roles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -551,8 +551,15 @@ give the layout such a name:
a verdict has the color of its value.

The rows stay in the sphinx-needs order (head, fields, text) in the HTML; the
CSS shows the text before the fields. So the PDF and a screen reader still
give the fields first.
CSS shows the text before the fields. So a screen reader still gives the
fields first.

In a PDF, ``need_cards`` writes the card with the LaTeX macros of
``zdocs.sty``: a bar in the accent color of the type on the left (no
background, so that it prints well), the head with the pill as a framed
badge, the title in bold, the text, then the fields and links with their
labels in one column. :doc:`../howto/change-the-look-of-the-pdf` tells how to
change it.

The ``need_cards`` extension (loaded by ``zdocs_conf``) gives each card the
``color`` of its need type in ``needs_config.toml`` as the accent, and the
Expand Down
27 changes: 27 additions & 0 deletions sphinx/_extensions/_tests/roots/test-needcards-latex/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright (c) 2026 inovex GmbH
#
# SPDX-License-Identifier: Apache-2.0

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/
sys.path.insert(0, str(Path(__file__).resolve().parents[4] / "_scripts"))

from need_layouts import need_layout # noqa: E402

extensions = ["sphinx_needs", "need_cards"]
master_doc = "index"
exclude_patterns = ["_build"]
suppress_warnings = ["config.cache"]
latex_documents = [("index", "cards.tex", "Cards", "zdocs", "howto")]

needs_types = [
dict(directive="req", title="Requirement", prefix="REQ_", color="#FDEBD0", style="node"),
dict(directive="spec", title="Test Case", prefix="SPEC_", color="", style="node"),
]
needs_fields = {"component": {}}
needs_layouts = {
"zdocs_card": need_layout(fields=[("component", "Component")]),
}
needs_default_layout = "zdocs_card"
18 changes: 18 additions & 0 deletions sphinx/_extensions/_tests/roots/test-needcards-latex/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Need cards in a PDF
===================

.. req:: A requirement
:id: REQ_1
:status: approved
:component: storage

The text of the requirement.

.. spec:: A test case
:id: SPEC_A_LONG_ID
:status: passed
:links: REQ_1

.. req:: Not a card
:id: REQ_2
:layout: clean
21 changes: 21 additions & 0 deletions sphinx/_extensions/_tests/roots/test-pdftables/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright (c) 2026 inovex GmbH
#
# SPDX-License-Identifier: Apache-2.0

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # _extensions/

extensions = ["sphinx_needs", "pdf_tables"]
master_doc = "index"
exclude_patterns = ["_build"]
suppress_warnings = ["config.cache"]
latex_documents = [("index", "tables.tex", "Tables", "zdocs", "howto")]

needs_types = [
dict(directive="req", title="Requirement", prefix="REQ_", color="#FDEBD0", style="node"),
]
_field = {"schema": {"type": "string"}, "nullable": True}
needs_fields = {name: dict(_field) for name in ("platform", "scenario", "module", "reason")}
needs_id_regex = r"^[A-Z0-9_-]{5,}"
23 changes: 23 additions & 0 deletions sphinx/_extensions/_tests/roots/test-pdftables/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Need tables in a PDF
====================

.. req:: Sealing on initialisation
:id: SD-REQ-001
:tags: core; kconfig-SAFE_DATA_LOCK_TIMEOUT_MS
:platform: native_sim/native/64
:scenario: safe_data.api.timeout
:module: tests/safe_data
:reason: ztest skip

.. needtable::
:columns: id, title, status, tags
:style: table

.. needtable::
:columns: id, title, status, tags, platform, scenario, module, reason
:style: table

.. needtable::
:columns: id, title
:colwidths: 30, 70
:style: table
57 changes: 57 additions & 0 deletions sphinx/_extensions/_tests/test_need_cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from pathlib import Path

import need_cards
import pytest
from need_cards import (
_JS_SOURCE,
Expand Down Expand Up @@ -95,3 +96,59 @@ def link(href_part):
assert NEED_REF_CLASS in link('href="#REQ_1"') # the links field of SPEC_1
assert NEED_REF_CLASS not in link("main_8c_source.html#l00042")
assert f"a.{NEED_REF_CLASS}" in _JS_SOURCE.read_text()


# -- The card in a PDF ---------------------------------------------------------

_LATEX_ROOT = str(Path(__file__).parent / "roots" / "test-needcards-latex")


@pytest.mark.parametrize(
"color, rgb",
[
("#FDEBD0", "8B6001"), # the type color of a requirement: brown-orange
("#D6EAF8", "0571A3"),
("#eeeeee", "696969"), # a grey stays grey
("#fff", "696969"),
("", None),
("red", None), # a name: no hue that the engine can compute
],
)
def test_the_accent_of_a_type_color(color, rgb):
assert need_cards.accent(color) == rgb


@pytest.mark.parametrize(
"cls, tone",
[
("zd-status-passed", "good"),
("zd-status-failed", "bad"),
("zd-verdict-partial", "gap"),
("zd-status-skipped", "none"),
("zd-status-approved", "accent"),
],
)
def test_the_tone_of_a_status(cls, tone):
assert need_cards.tone(cls) == tone


@pytest.mark.sphinx("latex", srcdir=_LATEX_ROOT)
def test_a_card_is_written_with_the_macros_of_zdocs_sty(app):
app.build()
tex = (Path(app.outdir) / "cards.tex").read_text()
req = tex[tex.index("\\begin{zdneed}{8B6001}") :]
req = req[: req.index("\\end{zdneed}")]
assert "\\phantomsection\\label{\\detokenize{index:REQ_1}}" in req
assert "\\zdneedhead{Requirement}{REQ\\_\\allowbreak{}1}{accent}{approved}" in req
assert "\\zdneedtitle{" in req and "A requirement" in req
# The title, then the text, then the fields and links.
text = req.index("The text of the requirement.")
assert req.index("\\zdneedtitle{") < text < req.index("\\zdneedfield{Component}")
assert req.index("\\zdneedfield{Component}") < req.index("\\zdneedfield{links incoming}")
assert "SPEC\\_A\\_LONG\\_ID" in req[req.index("\\zdneedfield{links incoming}") :]

spec = tex[tex.index("\\begin{zdneed}{666666}") :]
assert spec.index("{good}{passed}") < spec.index("\\end{zdneed}")
# A need of another layout keeps the sphinx-needs table.
assert tex.count("\\begin{zdneed}") == 2
assert "\\label{\\detokenize{index:REQ_2}}" in tex
Loading
Loading