diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index cfc5db065..e5ab9c7c0 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -1904,7 +1904,7 @@ These options influence LaTeX output. This value determines how to group the document tree into LaTeX source files. It must be a list of tuples ``(startdocname, targetname, title, author, - documentclass, toctree_only)``, where the items are: + theme, toctree_only)``, where the items are: *startdocname* String that specifies the :term:`document name` of the LaTeX file's master @@ -1926,13 +1926,8 @@ These options influence LaTeX output. applies. Use ``\\and`` to separate multiple authors, as in: ``'John \\and Sarah'`` (backslashes must be Python-escaped to reach LaTeX). - *documentclass* - Normally, one of ``'manual'`` or ``'howto'`` (provided by Sphinx and based - on ``'report'``, resp. ``'article'``; Japanese documents use ``'jsbook'``, - resp. ``'jreport'``.) "howto" (non-Japanese) documents will not get - appendices. Also they have a simpler title page. Other document classes - can be given. Independently of the document class, the "sphinx" package is - always loaded in order to define Sphinx's custom LaTeX commands. + *theme* + LaTeX theme. See :confval:`latex_theme`. *toctree_only* Must be ``True`` or ``False``. If true, the *startdoc* document itself is @@ -2087,6 +2082,33 @@ These options influence LaTeX output. This overrides the files which is provided from Sphinx such as ``sphinx.sty``. +.. confval:: latex_theme + + The "theme" that the LaTeX output should use. It is a collection of settings + for LaTeX output (ex. document class, top level sectioning unit and so on). + + As a built-in LaTeX themes, ``manual`` and ``howto`` are bundled. + + ``manual`` + A LaTeX theme for writing a manual. It imports the ``report`` document + class (Japanese documents use ``jsbook``). + + ``howto`` + A LaTeX theme for writing an article. It imports the ``article`` document + class (Japanese documents use ``jreport`` rather). :confval:`latex_appendices` + is available only for this theme. + + It defaults to ``'manual'``. + + .. versionadded:: 3.0 + +.. confval:: latex_theme_path + + A list of paths that contain custom LaTeX themes as subdirectories. Relative + paths are taken as relative to the configuration directory. + + .. versionadded:: 3.0 + .. _text-options: diff --git a/sphinx/builders/latex/__init__.py b/sphinx/builders/latex/__init__.py index fbe1ff2bf..354b20284 100644 --- a/sphinx/builders/latex/__init__.py +++ b/sphinx/builders/latex/__init__.py @@ -493,7 +493,7 @@ def default_latex_documents(config: Config) -> List[Tuple[str, str, str, str, st make_filename_from_project(config.project) + '.tex', texescape.escape_abbr(project), texescape.escape_abbr(author), - 'manual')] + config.latex_theme)] def setup(app: Sphinx) -> Dict[str, Any]: @@ -516,6 +516,8 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('latex_show_pagerefs', False, None) app.add_config_value('latex_elements', {}, None) app.add_config_value('latex_additional_files', [], None) + app.add_config_value('latex_theme', 'manual', None, [str]) + app.add_config_value('latex_theme_path', [], None, [list]) app.add_config_value('latex_docclass', default_latex_docclass, None) diff --git a/sphinx/builders/latex/theming.py b/sphinx/builders/latex/theming.py index c7c76d829..56f2735f0 100644 --- a/sphinx/builders/latex/theming.py +++ b/sphinx/builders/latex/theming.py @@ -8,10 +8,17 @@ :license: BSD, see LICENSE for details. """ +import configparser +from os import path from typing import Dict from sphinx.application import Sphinx from sphinx.config import Config +from sphinx.errors import ThemeError +from sphinx.locale import __ +from sphinx.util import logging + +logger = logging.getLogger(__name__) class Theme: @@ -56,11 +63,30 @@ class BuiltInTheme(Theme): return 'chapter' +class UserTheme(Theme): + """A user defined LaTeX theme.""" + + def __init__(self, name: str, filename: str) -> None: + self.name = name + self.config = configparser.RawConfigParser() + self.config.read(path.join(filename)) + + try: + self.docclass = self.config.get('theme', 'docclass') + self.wrapperclass = self.config.get('theme', 'wrapperclass') + self.toplevel_sectioning = self.config.get('theme', 'toplevel_sectioning') + except configparser.NoSectionError: + raise ThemeError(__('%r doesn\'t have "theme" setting') % filename) + except configparser.NoOptionError as exc: + raise ThemeError(__('%r doesn\'t have "%s" setting') % (filename, exc.args[0])) + + class ThemeFactory: """A factory class for LaTeX Themes.""" def __init__(self, app: Sphinx) -> None: self.themes = {} # type: Dict[str, Theme] + self.theme_paths = [path.join(app.srcdir, p) for p in app.config.latex_theme_path] self.load_builtin_themes(app.config) def load_builtin_themes(self, config: Config) -> None: @@ -70,7 +96,23 @@ class ThemeFactory: def get(self, name: str) -> Theme: """Get a theme for given *name*.""" - if name not in self.themes: - return Theme(name) - else: + if name in self.themes: return self.themes[name] + else: + theme = self.find_user_theme(name) + if theme: + return theme + else: + return Theme(name) + + def find_user_theme(self, name: str) -> Theme: + """Find a theme named as *name* from latex_theme_path.""" + for theme_path in self.theme_paths: + config_path = path.join(theme_path, name, 'theme.conf') + if path.isfile(config_path): + try: + return UserTheme(name, config_path) + except ThemeError as exc: + logger.warning(exc) + + return None diff --git a/tests/roots/test-latex-theme/conf.py b/tests/roots/test-latex-theme/conf.py new file mode 100644 index 000000000..196307ae6 --- /dev/null +++ b/tests/roots/test-latex-theme/conf.py @@ -0,0 +1,2 @@ +latex_theme = 'custom' +latex_theme_path = ['theme'] diff --git a/tests/roots/test-latex-theme/index.rst b/tests/roots/test-latex-theme/index.rst new file mode 100644 index 000000000..f5b1d5380 --- /dev/null +++ b/tests/roots/test-latex-theme/index.rst @@ -0,0 +1,2 @@ +latex_theme +=========== diff --git a/tests/roots/test-latex-theme/theme/custom/theme.conf b/tests/roots/test-latex-theme/theme/custom/theme.conf new file mode 100644 index 000000000..8961fac75 --- /dev/null +++ b/tests/roots/test-latex-theme/theme/custom/theme.conf @@ -0,0 +1,4 @@ +[theme] +docclass = book +wrapperclass = sphinxbook +toplevel_sectioning = chapter diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 88c79b5ed..d55a3b895 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -20,7 +20,7 @@ from test_build_html import ENV_WARNINGS from sphinx.builders.latex import default_latex_documents from sphinx.config import Config -from sphinx.errors import SphinxError +from sphinx.errors import SphinxError, ThemeError from sphinx.testing.util import strip_escseq from sphinx.util import docutils from sphinx.util.osutil import cd, ensuredir @@ -215,6 +215,15 @@ def test_latex_basic_howto_ja(app, status, warning): assert r'\documentclass[letterpaper,10pt,dvipdfmx]{sphinxhowto}' in result +@pytest.mark.sphinx('latex', testroot='latex-theme') +def test_latex_theme(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'python.tex').read_text(encoding='utf8') + print(result) + assert r'\def\sphinxdocclass{book}' in result + assert r'\documentclass[letterpaper,10pt,english]{sphinxbook}' in result + + @pytest.mark.sphinx('latex', testroot='basic', confoverrides={'language': 'zh'}) def test_latex_additional_settings_for_language_code(app, status, warning): app.builder.build_all() @@ -1465,6 +1474,7 @@ def test_default_latex_documents(): 'author': "Wolfgang Schäuble & G'Beckstein."}) config.init_values() config.add('latex_engine', None, True, None) + config.add('latex_theme', 'manual', True, None) expected = [('index', 'stasi.tex', 'STASI™ Documentation', r"Wolfgang Schäuble \& G\textquotesingle{}Beckstein.\@{}", 'manual')] assert default_latex_documents(config) == expected