Source code for sphinx_toolbox.latex.layout

#!/usr/bin/env python3
#
#  layout.py
r"""
Makes minor adjustments to the LaTeX layout.

* Increases the whitespace above function signatures by 5px,
  to prevent the function visually merging with the previous one.
* Remove unnecessary indentation and allow "raggedright" for the fields in
  the body of functions, which prevents ugly whitespace and line breaks.
* Disables justification for function signatures.
  This is a backport of changes from Sphinx 4 added in :github:pull:`8997 <sphinx-doc/sphinx>`.

  .. versionadded:: 2.12.0

* With Sphinx 3.5, doesn't add ``\sphinxAtStartPar`` before every paragraph.
  The change in :github:issue:`8781 <sphinx-doc/sphinx>` was to solve an issue with *tables*,
  but it isn't clear why it then gets added for *every* paragraph so this extension removes it.

  .. versionadded:: 2.13.0

* Configures hyperref to apply correct page numbering to the frontmatter.

  .. versionadded:: 2.14.0

* Optionally, configures the ``needspace`` package.

  The :confval:`needspace_amount` option can be set in ``conf.py`` to add the ``\needspace{}`` command
  before each ``addnodes.desc`` node (i.e. a function or class description).
  The amount of space is set by the ``needspace_amount`` option, e.g.:

  .. code-block:: python

      needspace_amount = r"4\baselineskip"

  .. versionadded:: 3.0.0


.. confval:: needspace_amount
	:type: :class:`str`
	:default: :py:obj:`None`

	Sets the value for the ``\needspace{}`` command.

	Values should be in the form ``r"4\baselineskip"``.

	.. versionadded:: 3.0.0


.. versionadded:: 2.10.0
.. versionchanged:: 3.0.0  Moved from :mod:`sphinx_toolbox.tweaks.latex_layout`.
.. latex:clearpage::
.. extensions:: sphinx_toolbox.latex.layout

-----

"""
#
#  Copyright © 2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#

# 3rd party
from docutils import nodes
from domdf_python_tools.stringlist import StringList
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.builders.latex.nodes import footnotetext
from sphinx.config import Config
from sphinx.writers.latex import LaTeXTranslator

# this package
from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version

__all__ = ("setup", )


def visit_desc(translator: LaTeXTranslator, node: addnodes.desc) -> None:
	translator.body.append('\n\n\\vspace{5px}')

	needspace_amount = getattr(translator.config, "needspace_amount")
	if needspace_amount:
		translator.body.append(f"\\needspace{{{needspace_amount}}}")

	if "sphinxcontrib.toctree_plus" in translator.config.extensions:
		# 3rd party
		from sphinxcontrib import toctree_plus  # type: ignore[import]  # nodep

		toctree_plus.visit_desc(translator, node)
	else:
		LaTeXTranslator.visit_desc(translator, node)


def visit_field_list(translator: LaTeXTranslator, node: nodes.field_list) -> None:
	translator.body.append('\\vspace{10px}\\begin{flushleft}\\begin{description}\n')
	if translator.table:  # pragma: no cover
		translator.table.has_problematic = True


def depart_field_list(translator: LaTeXTranslator, node: nodes.field_list) -> None:
	translator.body.append('\\end{description}\\end{flushleft}\\vspace{10px}\n')


def configure(app: Sphinx, config: Config) -> None:
	"""
	Configure Sphinx Extension.

	:param app: The Sphinx application.
	:param config:
	"""

	if not hasattr(config, "latex_elements"):  # pragma: no cover
		config.latex_elements = {}  # type: ignore[attr-defined]

	latex_elements = (config.latex_elements or {})

	latex_preamble = latex_elements.get("preamble", '')

	# Backported from Sphinx 4
	# See https://github.com/sphinx-doc/sphinx/pull/8997
	config.latex_elements["preamble"] = '\n'.join([
			latex_preamble,
			r"\makeatletter",
			'',
			r"\renewcommand{\py@sigparams}[2]{%",
			r"  \parbox[t]{\py@argswidth}{\raggedright #1\sphinxcode{)}#2\strut}%",
			"  % final strut is to help get correct vertical separation in case of multi-line",
			"  % box with the item contents.",
			'}',
			r"\makeatother",
			])

	config.latex_elements["hyperref"] = '\n'.join([
			r"% Include hyperref last.",
			r"\usepackage[pdfpagelabels,hyperindex,hyperfigures]{hyperref}",
			r"% Fix anchor placement for figures with captions.",
			r"\usepackage{hypcap}% it must be loaded after hyperref.",
			])

	config.latex_elements["maketitle"] = '\n'.join([
			r"\begingroup",
			r"\let\oldthepage\thepage",
			r"\renewcommand{\thepage}{T\oldthepage}",
			config.latex_elements.get("maketitle", r"\sphinxmaketitle"),
			r"\endgroup"
			])

	needspace_amount = getattr(config, "needspace_amount")
	if needspace_amount:
		latex_extrapackages = StringList(latex_elements.get("extrapackages", ''))
		latex_extrapackages.append(r"\usepackage{needspace}")
		latex_elements["extrapackages"] = str(latex_extrapackages)


def visit_paragraph(translator: LaTeXTranslator, node: nodes.paragraph) -> None:
	index = node.parent.index(node)
	if (
			index > 0 and isinstance(node.parent, nodes.compound)
			and not isinstance(node.parent[index - 1], nodes.paragraph)
			and not isinstance(node.parent[index - 1], nodes.compound)
			):
		# insert blank line, if the paragraph follows a non-paragraph node in a compound
		translator.body.append("\\noindent\n")
	elif index == 1 and isinstance(node.parent, (nodes.footnote, footnotetext)):
		# don't insert blank line, if the paragraph is second child of a footnote
		# (first one is label node)
		pass
	else:
		# Sphinx 3.5 adds \sphinxAtStartPar here, but I don't see what it gains.
		translator.body.append('\n')


[docs]@metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.latex.layout`. :param app: The Sphinx application. """ app.connect("config-inited", configure, priority=500) app.add_node(addnodes.desc, latex=(visit_desc, LaTeXTranslator.depart_desc), override=True) app.add_node(nodes.field_list, latex=(visit_field_list, depart_field_list), override=True) app.add_node(nodes.paragraph, latex=(visit_paragraph, LaTeXTranslator.depart_paragraph), override=True) app.add_config_value("needspace_amount", default=None, rebuild="latex", types=[str]) return {"parallel_read_safe": True}