Skip to content

LaTeX: support CSS3 length units #13657

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 6, 2025
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ jobs:
enable-cache: false
- name: Install dependencies
run: uv pip install . --group test
- name: Install Docutils' HEAD
run: uv pip install "docutils @ git+https://repo.or.cz/docutils.git#subdirectory=docutils"
- name: Test with pytest
run: python -m pytest -vv --durations 25
env:
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ Features added
Patch by Adam Turner.
* #13647: LaTeX: allow more cases of table nesting.
Patch by Jean-François B.
* #13657: LaTeX: support CSS3 length units.
Patch by Jean-François B.
* #13684: intersphinx: Add a file-based cache for remote inventories.
The location of the cache directory must not be relied upon externally,
as it may change without notice or warning in future releases.
Expand Down
1 change: 1 addition & 0 deletions sphinx/templates/latex/latex.tex.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
\ifdefined\pdfimageresolution
\pdfimageresolution= \numexpr \dimexpr1in\relax/\sphinxpxdimen\relax
\fi
\newdimen\sphinxremdimen\sphinxremdimen = <%= pointsize%>
%% let collapsible pdf bookmarks panel have high depth per default
\PassOptionsToPackage{bookmarksdepth=5}{hyperref}
<% if use_xindy -%>
Expand Down
22 changes: 21 additions & 1 deletion sphinx/texinputs/sphinx.sty
Original file line number Diff line number Diff line change
Expand Up @@ -1241,5 +1241,25 @@
% FIXME: this line should be dropped, as "9" is default anyhow.
\ifdefined\pdfcompresslevel\pdfcompresslevel = 9 \fi


%%% SUPPORT FOR CSS3 EXTRA LENGTH UNITS
% cf rstdim_to_latexdim in latex.py
%
\def\sphinxchdimen{\dimexpr\fontcharwd\font`0\relax}
% TODO: decide if we want rather \textwidth/\textheight.
\newdimen\sphinxvwdimen
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LaTeXnical note: we could define (via a suitable \edef) \sphinxvwdimen and all the others as a macro expanding to some \dimexpr...\relax, but since TeXLive 2003 pdflatex embeds e-TeX extensions (last released in 1999, but LaTeX documentation in books or online is woefully outdated and almost never mention them) and, besides providing \dimexpr and \numexpr, these extensions have lifted the restriction on the number of \newdimen one can use. e-TeX also provided \fontcharwd used above in the \sphinxchdimen, it will dynamically adapt to the font used at that point in document; although Sphinx documents will not likely change fonts, but if in a footnote this will take care of the smaller font size.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

% TODO: decide if we want rather \textwidth / \textheight.

As in HTML/CSS, vw and vh relate to the viewport (~ screen size) and not the text area, I recommend keeping the conversion to \paperwidth / \paperheight.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In HTML/CSS the viewport can be fully occupied, not really in LaTeX apart from margin notes for which we have no interface. This is why \textwidth and \textheight are a serious contender in my opinion. I do not have strong opinion though.

\sphinxvwdimen=\dimexpr0.01\paperwidth\relax
\newdimen\sphinxvhdimen
\sphinxvhdimen=\dimexpr0.01\paperheight\relax
\newdimen\sphinxvmindimen
\sphinxvmindimen=\dimexpr
\ifdim\paperwidth<\paperheight\sphinxvwdimen\else\sphinxvhdimen\fi
\relax
\newdimen\sphinxvmaxdimen
\sphinxvmaxdimen=\dimexpr
\ifdim\paperwidth<\paperheight\sphinxvhdimen\else\sphinxvwdimen\fi
\relax
\newdimen\sphinxQdimen
\sphinxQdimen=0.25mm
% MEMO: \sphinxremdimen is defined in the template as it needs
% the config variable pointsize.
\endinput
10 changes: 10 additions & 0 deletions sphinx/writers/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ def escape_abbr(text: str) -> str:

def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
"""Convert `width_str` with rst length to LaTeX length."""
# MEMO: the percent unit is interpreted here as a percentage
# of \linewidth. Let's keep in mind though that \linewidth
# is dynamic in LaTeX, e.g. it is smaller in lists.
match = re.match(r'^(\d*\.?\d*)\s*(\S*)$', width_str)
if not match:
raise ValueError
Expand All @@ -310,6 +313,8 @@ def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
res = '%sbp' % amount # convert to 'bp'
elif unit == '%':
res = r'%.3f\linewidth' % (float(amount) / 100.0)
elif unit in {'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'Q'}:
res = rf'{amount}\sphinx{unit}dimen'
else:
amount_float = float(amount) * scale / 100.0
if unit in {'', 'px'}:
Expand All @@ -318,8 +323,13 @@ def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
res = '%.5fbp' % amount_float
elif unit == '%':
res = r'%.5f\linewidth' % (amount_float / 100.0)
elif unit in {'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'Q'}:
res = rf'{amount_float:.5f}\sphinx{unit}dimen'
else:
res = f'{amount_float:.5f}{unit}'
# Those further units are passed through and accepted "as is" by TeX:
# em and ex (both font dependent), bp, cm, mm, in, and pc.
# Non-CSS units (TeX only presumably) are cc, nc, dd, nd, and sp.
return res


Expand Down
Empty file.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions tests/roots/test-latex-images-css3-lengths/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
=============
TEST IMAGES
=============

test-latex-images-css3-lengths
==============================

.. image:: img.png
:width: 10.03ch
:height: 9.97rem

.. image:: img.png
:width: 60vw
:height: 10vh

.. image:: img.png
:width: 10.5vmin
:height: 10.5vmax

.. image:: img.png
:width: 195.345Q

.. image:: img.png
:width: 195.345Q
:scale: 50%
46 changes: 35 additions & 11 deletions tests/test_builders/test_build_latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ def kpsetest(*filenames):


# compile latex document with app.config.latex_engine
def compile_latex_document(app, filename='projectnamenotset.tex', docclass='manual'):
def compile_latex_document(
app, filename='projectnamenotset.tex', docclass='manual', runtwice=False
):
# now, try to run latex over it
try:
with chdir(app.outdir):
Expand All @@ -82,7 +84,7 @@ def compile_latex_document(app, filename='projectnamenotset.tex', docclass='manu
# as configured in the Makefile and in presence of latexmkrc
# or latexmkjarc and also sphinx.xdy and other xindy support.
# And two passes are not enough except for simplest documents.
if app.config.latex_engine == 'pdflatex':
if runtwice:
subprocess.run(args, capture_output=True, check=True)
except OSError as exc: # most likely the latex executable was not found
raise pytest.skip.Exception from exc
Expand All @@ -101,6 +103,10 @@ def compile_latex_document(app, filename='projectnamenotset.tex', docclass='manu
not kpsetest(*STYLEFILES),
reason='not running latex, the required styles do not seem to be installed',
)
skip_if_docutils_not_at_least_at_0_22 = pytest.mark.skipif(
docutils.__version_info__[:2] < (0, 22),
reason='this test requires Docutils at least at 0.22',
)


class RemoteImageHandler(http.server.BaseHTTPRequestHandler):
Expand Down Expand Up @@ -128,25 +134,27 @@ def do_GET(self):
@skip_if_requested
@skip_if_stylefiles_notfound
@pytest.mark.parametrize(
('engine', 'docclass', 'python_maximum_signature_line_length'),
('engine', 'docclass', 'python_maximum_signature_line_length', 'runtwice'),
# Only running test with `python_maximum_signature_line_length` not None with last
# LaTeX engine to reduce testing time, as if this configuration does not fail with
# one engine, it's almost impossible it would fail with another.
[
('pdflatex', 'manual', None),
('pdflatex', 'howto', None),
('lualatex', 'manual', None),
('lualatex', 'howto', None),
('xelatex', 'manual', 1),
('xelatex', 'howto', 1),
('pdflatex', 'manual', None, True),
('pdflatex', 'howto', None, True),
('lualatex', 'manual', None, False),
('lualatex', 'howto', None, False),
('xelatex', 'manual', 1, False),
('xelatex', 'howto', 1, False),
],
)
@pytest.mark.sphinx(
'latex',
testroot='root',
freshenv=True,
)
def test_build_latex_doc(app, engine, docclass, python_maximum_signature_line_length):
def test_build_latex_doc(
app, engine, docclass, python_maximum_signature_line_length, runtwice
):
app.config.python_maximum_signature_line_length = (
python_maximum_signature_line_length
)
Expand All @@ -170,7 +178,23 @@ def test_build_latex_doc(app, engine, docclass, python_maximum_signature_line_le
# file from latex_additional_files
assert (app.outdir / 'svgimg.svg').is_file()

compile_latex_document(app, 'sphinxtests.tex', docclass)
compile_latex_document(app, 'sphinxtests.tex', docclass, runtwice)


@skip_if_requested
@skip_if_stylefiles_notfound
@skip_if_docutils_not_at_least_at_0_22
@pytest.mark.parametrize('engine', ['pdflatex', 'lualatex', 'xelatex'])
@pytest.mark.sphinx(
'latex',
testroot='latex-images-css3-lengths',
)
def test_build_latex_with_css3_lengths(app, engine):
app.config.latex_engine = engine
app.config.latex_documents = [(*app.config.latex_documents[0][:4], 'howto')]
app.builder.init()
app.build(force_all=True)
compile_latex_document(app, docclass='howto')


@pytest.mark.sphinx('latex', testroot='root')
Expand Down
Loading