Skip to content

Commit 030b8f3

Browse files
Sanjays2402claude
authored andcommitted
Fix \title, \author and \date with an optional short argument
\title[short]{full} (and the same for \author and \date) was parsed with the optional argument missing from the macro spec, so the leading "[" and the short text were left as ordinary character nodes and the mandatory argument was never recognised. latex_to_text() then emitted the raw "short]full" text, and \maketitle rendered a stray "[". The optional argument is not a LaTeX kernel feature: the standard classes (article, report, book) declare these three commands with a single mandatory argument. Several widely used classes do add a leading optional argument holding a short form of the field, meant for running heads -- beamer for all three commands, and the AMS classes (amsart, amsbook, amsproc, and acmart, which is built on amsart) for \title and \author. Since documents using the standard classes simply never have the optional argument present, declaring it costs them nothing. Declare it in the latexwalker specs ('[{' instead of '{'), and have the latex2text specs read the *last* argument rather than a fixed position, so that they also keep working with a latex context in which these macros were declared with the mandatory argument alone. Adds regression tests at the latexwalker level, for the rendered title block with the short form, and for a context declaring the mandatory argument alone. Closes #114. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbTg7TMbpkWihLzotNLPdu
1 parent 22f719a commit 030b8f3

4 files changed

Lines changed: 118 additions & 7 deletions

File tree

‎pylatexenc/latex2text/_defaultspecs.py‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -579,12 +579,20 @@ def _format_frac(n, l2tobj):
579579
# later, by '\maketitle', so what is stored has to be a string; in math
580580
# mode the 'fancy' math mode renders a node list to a math
581581
# piece rather than to a string
582+
#
583+
# the argnlist[-1:] slices: the default specs declare these macros with
584+
# a leading optional argument, the "short" form of the field that
585+
# beamer and the AMS classes accept (see the macro specs in
586+
# pylatexenc/latexwalker/_defaultspecs.py), so the field itself is the
587+
# *last* argument. Taking the last one rather than a fixed position
588+
# also keeps these formatters working with a latex context in which the
589+
# macros were declared with the mandatory argument alone.
582590
('title', lambda n, l2tobj: \
583-
setattr(l2tobj, '_doc_title', str(l2tobj.nodelist_to_text(n.nodeargd.argnlist[0:1])))),
591+
setattr(l2tobj, '_doc_title', str(l2tobj.nodelist_to_text(n.nodeargd.argnlist[-1:])))),
584592
('author', lambda n, l2tobj: \
585-
setattr(l2tobj, '_doc_author', str(l2tobj.nodelist_to_text(n.nodeargd.argnlist[0:1])))),
593+
setattr(l2tobj, '_doc_author', str(l2tobj.nodelist_to_text(n.nodeargd.argnlist[-1:])))),
586594
('date', lambda n, l2tobj: \
587-
setattr(l2tobj, '_doc_date', str(l2tobj.nodelist_to_text(n.nodeargd.argnlist[0:1])))),
595+
setattr(l2tobj, '_doc_date', str(l2tobj.nodelist_to_text(n.nodeargd.argnlist[-1:])))),
588596
('maketitle', lambda n, l2tobj: \
589597
_format_maketitle(getattr(l2tobj, '_doc_title', r'[NO \title GIVEN]'),
590598
getattr(l2tobj, '_doc_author', r'[NO \author GIVEN]'),

‎pylatexenc/latexwalker/_defaultspecs.py‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,24 @@ def _finalize_lstlisting_environment_node(node):
206206

207207
MacroSpec('mbox', arguments_spec_list=[ _arg_textmode('{') ]),
208208

209-
# \title, \author, \date
210-
MacroSpec('title', '{'),
211-
MacroSpec('author', '{'),
212-
MacroSpec('date', '{'),
209+
# \title, \author, \date.
210+
#
211+
# The standard classes (article, report, book) declare these with a
212+
# single mandatory argument. A number of widely used classes add a
213+
# leading optional argument that holds a short form of the field,
214+
# meant for running heads and similar places:
215+
#
216+
# \title[Short title]{The full and rather long title}
217+
#
218+
# beamer does this for all three commands; the AMS classes (amsart,
219+
# amsbook, amsproc — and acmart, which is built on amsart) do it for
220+
# \title and \author. Documents that use the standard classes
221+
# simply never have the optional argument present, so declaring it
222+
# here costs them nothing while letting the common non-standard
223+
# forms parse correctly.
224+
MacroSpec('title', '[{'),
225+
MacroSpec('author', '[{'),
226+
MacroSpec('date', '[{'),
213227

214228
# (Note: single backslash) end of line with optional no-break ('*') and
215229
# additional vertical spacing, e.g. \\*[2mm]

‎test/test_2_latex2text.py‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import pytest
1717

1818
from pylatexenc.latexwalker import LatexWalker
19+
from pylatexenc import macrospec
1920
from pylatexenc import latex2text
2021
from pylatexenc.latex2text import LatexNodes2Text
2122

@@ -1831,6 +1832,67 @@ def test_repl_doc_title(self):
18311832
""" % { 'today': today, 'eqhrule': eqhrule }
18321833
)
18331834

1835+
def test_repl_doc_title_optional_short_arg(self):
1836+
1837+
# \title[short]{full}, \author[short]{full} and \date[short]{full}, as
1838+
# accepted by beamer and by the AMS classes: the optional "short"
1839+
# argument must be consumed as an argument, and the mandatory argument
1840+
# is the one that goes into the rendered title block.
1841+
1842+
self.assertEqualUpToWhitespace(
1843+
LatexNodes2Text().latex_to_text(
1844+
r"""
1845+
\title[Short Title]{The Title}
1846+
\author[T. A.]{The Author(s)}
1847+
\date[2020]{July 4, 2020}
1848+
\maketitle
1849+
"""
1850+
),
1851+
r"""
1852+
The Title
1853+
The Author(s)
1854+
July 4, 2020
1855+
=================
1856+
"""
1857+
)
1858+
1859+
def test_repl_doc_title_macros_declared_without_optional_arg(self):
1860+
1861+
# the \title/\author/\date text specs pick the *last* argument, so they
1862+
# keep working with a latex context in which these macros were declared
1863+
# with the mandatory argument alone (as pylatexenc 2 did)
1864+
1865+
latex_context = macrospec.LatexContextDb()
1866+
latex_context.add_context_category(
1867+
'my-title-macros',
1868+
macros=[
1869+
macrospec.MacroSpec('title', '{'),
1870+
macrospec.MacroSpec('author', '{'),
1871+
macrospec.MacroSpec('date', '{'),
1872+
macrospec.MacroSpec('maketitle', ''),
1873+
]
1874+
)
1875+
lw = LatexWalker(
1876+
r"""
1877+
\title{The Title}
1878+
\author{The Author(s)}
1879+
\date{July 4, 2020}
1880+
\maketitle
1881+
""",
1882+
latex_context=latex_context
1883+
)
1884+
(nodelist, npos, nlen) = lw.get_latex_nodes()
1885+
1886+
self.assertEqualUpToWhitespace(
1887+
LatexNodes2Text().nodelist_to_text(nodelist),
1888+
r"""
1889+
The Title
1890+
The Author(s)
1891+
July 4, 2020
1892+
=================
1893+
"""
1894+
)
1895+
18341896

18351897
def test_math_alphabets(self):
18361898

‎test/test_2_latexwalker.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2170,6 +2170,33 @@ def test_bug_issueno57(self):
21702170
self.assertEqual(len(node.nodeargd.argnlist), 3)
21712171

21722172

2173+
def test_title_author_date_optional_short_argument(self):
2174+
# see issue #114. The default specs declare \title, \author and \date
2175+
# with the optional "short form" argument that beamer and the AMS
2176+
# classes accept, in addition to the mandatory argument.
2177+
2178+
for macroname in ('title', 'author', 'date'):
2179+
2180+
lw = LatexWalker('\\'+macroname+r'[Short]{The Full Form}',
2181+
tolerant_parsing=False)
2182+
(nodelist, npos, nlen) = lw.get_latex_nodes()
2183+
self.assertEqual(len(nodelist), 1)
2184+
argnlist = nodelist[0].nodeargd.argnlist
2185+
self.assertEqual(len(argnlist), 2)
2186+
self.assertEqual(argnlist[0].nodelist[0].chars, 'Short')
2187+
self.assertEqual(argnlist[1].nodelist[0].chars, 'The Full Form')
2188+
2189+
# the standard classes' form, with the optional argument absent
2190+
lw = LatexWalker('\\'+macroname+r'{The Full Form}',
2191+
tolerant_parsing=False)
2192+
(nodelist, npos, nlen) = lw.get_latex_nodes()
2193+
self.assertEqual(len(nodelist), 1)
2194+
argnlist = nodelist[0].nodeargd.argnlist
2195+
self.assertEqual(len(argnlist), 2)
2196+
self.assertTrue(argnlist[0] is None)
2197+
self.assertEqual(argnlist[1].nodelist[0].chars, 'The Full Form')
2198+
2199+
21732200
def test_href_url_argument_is_verbatim(self):
21742201
# the target of \href is read verbatim, because a URL may well contain
21752202
# characters that are special to LaTeX. A percent sign in particular

0 commit comments

Comments
 (0)