Skip to content

Commit 4298c30

Browse files
authored
Fix continuation line indentation in productionlists (sphinx-doc#13319)
1 parent f969041 commit 4298c30

File tree

7 files changed

+165
-91
lines changed

7 files changed

+165
-91
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ Bugs fixed
141141
Patch by Pavel Holica
142142
* #13273, #13318: Properly convert command-line overrides for Boolean types.
143143
Patch by Adam Turner.
144+
* #13302, #13319: Use the correct indentation for continuation lines
145+
in :rst:dir:`productionlist` directives.
146+
Patch by Adam Turner.
144147

145148
Testing
146149
-------

sphinx/writers/html5.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -696,16 +696,15 @@ def depart_literal(self, node: Element) -> None:
696696
def visit_productionlist(self, node: Element) -> None:
697697
self.body.append(self.starttag(node, 'pre'))
698698
productionlist = cast('Iterable[addnodes.production]', node)
699-
names = (production['tokenname'] for production in productionlist)
700-
maxlen = max(len(name) for name in names)
699+
maxlen = max(len(production['tokenname']) for production in productionlist)
701700
lastname = None
702701
for production in productionlist:
703702
if production['tokenname']:
704703
lastname = production['tokenname'].ljust(maxlen)
705704
self.body.append(self.starttag(production, 'strong', ''))
706705
self.body.append(lastname + '</strong> ::= ')
707706
elif lastname is not None:
708-
self.body.append('%s ' % (' ' * len(lastname)))
707+
self.body.append(' ' * (maxlen + 5))
709708
production.walkabout(self)
710709
self.body.append('\n')
711710
self.body.append('</pre>\n')

sphinx/writers/manpage.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,7 @@ def visit_productionlist(self, node: Element) -> None:
277277
self.in_productionlist += 1
278278
self.body.append('.sp\n.nf\n')
279279
productionlist = cast('Iterable[addnodes.production]', node)
280-
names = (production['tokenname'] for production in productionlist)
281-
maxlen = max(len(name) for name in names)
280+
maxlen = max(len(production['tokenname']) for production in productionlist)
282281
lastname = None
283282
for production in productionlist:
284283
if production['tokenname']:
@@ -288,7 +287,7 @@ def visit_productionlist(self, node: Element) -> None:
288287
self.body.append(self.defs['strong'][1])
289288
self.body.append(' ::= ')
290289
elif lastname is not None:
291-
self.body.append('%s ' % (' ' * len(lastname)))
290+
self.body.append(' ' * (maxlen + 5))
292291
production.walkabout(self)
293292
self.body.append('\n')
294293
self.body.append('\n.fi\n')

sphinx/writers/texinfo.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,16 +1309,15 @@ def unknown_departure(self, node: Node) -> None:
13091309
def visit_productionlist(self, node: Element) -> None:
13101310
self.visit_literal_block(None)
13111311
productionlist = cast('Iterable[addnodes.production]', node)
1312-
names = (production['tokenname'] for production in productionlist)
1313-
maxlen = max(len(name) for name in names)
1312+
maxlen = max(len(production['tokenname']) for production in productionlist)
13141313

13151314
for production in productionlist:
13161315
if production['tokenname']:
13171316
for id in production.get('ids'):
13181317
self.add_anchor(id, production)
13191318
s = production['tokenname'].ljust(maxlen) + ' ::='
13201319
else:
1321-
s = '%s ' % (' ' * maxlen)
1320+
s = ' ' * (maxlen + 4)
13221321
self.body.append(self.escape(s))
13231322
self.body.append(self.escape(production.astext() + '\n'))
13241323
self.depart_literal_block(None)

sphinx/writers/text.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -788,15 +788,14 @@ def depart_caption(self, node: Element) -> None:
788788
def visit_productionlist(self, node: Element) -> None:
789789
self.new_state()
790790
productionlist = cast('Iterable[addnodes.production]', node)
791-
names = (production['tokenname'] for production in productionlist)
792-
maxlen = max(len(name) for name in names)
791+
maxlen = max(len(production['tokenname']) for production in productionlist)
793792
lastname = None
794793
for production in productionlist:
795794
if production['tokenname']:
796795
self.add_text(production['tokenname'].ljust(maxlen) + ' ::=')
797796
lastname = production['tokenname']
798797
elif lastname is not None:
799-
self.add_text('%s ' % (' ' * len(lastname)))
798+
self.add_text(' ' * (maxlen + 4))
800799
self.add_text(production.astext() + self.nl)
801800
self.end_state(wrap=False)
802801
raise nodes.SkipNode
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Tests the productionlist directive."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
from docutils import nodes
7+
8+
from sphinx.addnodes import pending_xref
9+
from sphinx.testing import restructuredtext
10+
from sphinx.testing.util import assert_node, etree_parse
11+
12+
TYPE_CHECKING = False
13+
if TYPE_CHECKING:
14+
from collections.abc import Callable
15+
from pathlib import Path
16+
17+
from sphinx.testing.util import SphinxTestApp
18+
19+
20+
@pytest.mark.sphinx('html', testroot='productionlist')
21+
def test_productionlist(app: SphinxTestApp) -> None:
22+
app.build(force_all=True)
23+
24+
warnings = app.warning.getvalue().split('\n')
25+
assert len(warnings) == 2
26+
assert warnings[-1] == ''
27+
assert (
28+
'Dup2.rst:4: WARNING: duplicate token description of Dup, other instance in Dup1'
29+
in warnings[0]
30+
)
31+
32+
etree = etree_parse(app.outdir / 'index.html')
33+
nodes = list(etree.iter('ul'))
34+
assert len(nodes) >= 3
35+
36+
ul = nodes[2]
37+
cases = []
38+
for li in list(ul):
39+
li_list = list(li)
40+
assert len(li_list) == 1
41+
p = li_list[0]
42+
assert p.tag == 'p'
43+
text = str(p.text).strip(' :')
44+
p_list = list(p)
45+
assert len(p_list) == 1
46+
a = p_list[0]
47+
assert a.tag == 'a'
48+
link = a.get('href')
49+
a_list = list(a)
50+
assert len(a_list) == 1
51+
code = a_list[0]
52+
assert code.tag == 'code'
53+
code_list = list(code)
54+
assert len(code_list) == 1
55+
span = code_list[0]
56+
assert span.tag == 'span'
57+
assert span.text is not None
58+
link_text = span.text.strip()
59+
cases.append((text, link, link_text))
60+
assert cases == [
61+
('A', 'Bare.html#grammar-token-A', 'A'),
62+
('B', 'Bare.html#grammar-token-B', 'B'),
63+
('P1:A', 'P1.html#grammar-token-P1-A', 'P1:A'),
64+
('P1:B', 'P1.html#grammar-token-P1-B', 'P1:B'),
65+
('P2:A', 'P1.html#grammar-token-P1-A', 'P1:A'),
66+
('P2:B', 'P2.html#grammar-token-P2-B', 'P2:B'),
67+
('Explicit title A, plain', 'Bare.html#grammar-token-A', 'MyTitle'),
68+
('Explicit title A, colon', 'Bare.html#grammar-token-A', 'My:Title'),
69+
('Explicit title P1:A, plain', 'P1.html#grammar-token-P1-A', 'MyTitle'),
70+
('Explicit title P1:A, colon', 'P1.html#grammar-token-P1-A', 'My:Title'),
71+
('Tilde A', 'Bare.html#grammar-token-A', 'A'),
72+
('Tilde P1:A', 'P1.html#grammar-token-P1-A', 'A'),
73+
('Tilde explicit title P1:A', 'P1.html#grammar-token-P1-A', '~MyTitle'),
74+
('Tilde, explicit title P1:A', 'P1.html#grammar-token-P1-A', 'MyTitle'),
75+
('Dup', 'Dup2.html#grammar-token-Dup', 'Dup'),
76+
('FirstLine', 'firstLineRule.html#grammar-token-FirstLine', 'FirstLine'),
77+
('SecondLine', 'firstLineRule.html#grammar-token-SecondLine', 'SecondLine'),
78+
]
79+
80+
text = (app.outdir / 'LineContinuation.html').read_text(encoding='utf8')
81+
assert 'A</strong> ::= B C D E F G' in text
82+
83+
84+
@pytest.mark.sphinx('html', testroot='root')
85+
def test_productionlist_xref(app: SphinxTestApp) -> None:
86+
text = """\
87+
.. productionlist:: P2
88+
A: `:A` `A`
89+
B: `P1:B` `~P1:B`
90+
"""
91+
doctree = restructuredtext.parse(app, text)
92+
refnodes = list(doctree.findall(pending_xref))
93+
assert_node(refnodes[0], pending_xref, reftarget='A')
94+
assert_node(refnodes[1], pending_xref, reftarget='P2:A')
95+
assert_node(refnodes[2], pending_xref, reftarget='P1:B')
96+
assert_node(refnodes[3], pending_xref, reftarget='P1:B')
97+
assert_node(refnodes[0], [pending_xref, nodes.literal, 'A'])
98+
assert_node(refnodes[1], [pending_xref, nodes.literal, 'A'])
99+
assert_node(refnodes[2], [pending_xref, nodes.literal, 'P1:B'])
100+
assert_node(refnodes[3], [pending_xref, nodes.literal, 'B'])
101+
102+
103+
def test_productionlist_continuation_lines(
104+
make_app: Callable[..., SphinxTestApp], tmp_path: Path
105+
) -> None:
106+
text = """
107+
.. productionlist:: python-grammar
108+
assignment_stmt: (`target_list` "=")+ (`starred_expression` | `yield_expression`)
109+
target_list: `target` ("," `target`)* [","]
110+
target: `identifier`
111+
: | "(" [`target_list`] ")"
112+
: | "[" [`target_list`] "]"
113+
: | `attributeref`
114+
: | `subscription`
115+
: | `slicing`
116+
: | "*" `target`
117+
"""
118+
(tmp_path / 'conf.py').touch()
119+
(tmp_path / 'index.rst').write_text(text, encoding='utf-8')
120+
121+
app = make_app(buildername='text', srcdir=tmp_path)
122+
app.build(force_all=True)
123+
content = (app.outdir / 'index.txt').read_text(encoding='utf-8')
124+
expected = """\
125+
assignment_stmt ::= (target_list "=")+ (starred_expression | yield_expression)
126+
target_list ::= target ("," target)* [","]
127+
target ::= identifier
128+
| "(" [target_list] ")"
129+
| "[" [target_list] "]"
130+
| attributeref
131+
| subscription
132+
| slicing
133+
| "*" target
134+
"""
135+
assert content == expected
136+
137+
app = make_app(buildername='html', srcdir=tmp_path)
138+
app.build(force_all=True)
139+
content = (app.outdir / 'index.html').read_text(encoding='utf-8')
140+
_, _, content = content.partition('<pre>')
141+
content, _, _ = content.partition('</pre>')
142+
expected = """
143+
<strong id="grammar-token-python-grammar-assignment_stmt">assignment_stmt</strong> ::= (<a class="reference internal" href="#grammar-token-python-grammar-target_list"><code class="xref docutils literal notranslate"><span class="pre">target_list</span></code></a> &quot;=&quot;)+ (<code class="xref docutils literal notranslate"><span class="pre">starred_expression</span></code> | <code class="xref docutils literal notranslate"><span class="pre">yield_expression</span></code>)
144+
<strong id="grammar-token-python-grammar-target_list">target_list </strong> ::= <a class="reference internal" href="#grammar-token-python-grammar-target"><code class="xref docutils literal notranslate"><span class="pre">target</span></code></a> (&quot;,&quot; <a class="reference internal" href="#grammar-token-python-grammar-target"><code class="xref docutils literal notranslate"><span class="pre">target</span></code></a>)* [&quot;,&quot;]
145+
<strong id="grammar-token-python-grammar-target">target </strong> ::= <code class="xref docutils literal notranslate"><span class="pre">identifier</span></code>
146+
| &quot;(&quot; [<a class="reference internal" href="#grammar-token-python-grammar-target_list"><code class="xref docutils literal notranslate"><span class="pre">target_list</span></code></a>] &quot;)&quot;
147+
| &quot;[&quot; [<a class="reference internal" href="#grammar-token-python-grammar-target_list"><code class="xref docutils literal notranslate"><span class="pre">target_list</span></code></a>] &quot;]&quot;
148+
| <code class="xref docutils literal notranslate"><span class="pre">attributeref</span></code>
149+
| <code class="xref docutils literal notranslate"><span class="pre">subscription</span></code>
150+
| <code class="xref docutils literal notranslate"><span class="pre">slicing</span></code>
151+
| &quot;*&quot; <a class="reference internal" href="#grammar-token-python-grammar-target"><code class="xref docutils literal notranslate"><span class="pre">target</span></code></a>
152+
"""
153+
assert content == expected

tests/test_domains/test_domain_std.py

Lines changed: 1 addition & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
)
2222
from sphinx.domains.std import StandardDomain
2323
from sphinx.testing import restructuredtext
24-
from sphinx.testing.util import assert_node, etree_parse
24+
from sphinx.testing.util import assert_node
2525

2626

2727
def test_process_doc_handle_figure_caption():
@@ -485,84 +485,6 @@ def test_multiple_cmdoptions(app):
485485
assert domain.progoptions['cmd', '--output'] == ('index', 'cmdoption-cmd-o')
486486

487487

488-
@pytest.mark.sphinx('html', testroot='productionlist')
489-
def test_productionlist(app):
490-
app.build(force_all=True)
491-
492-
warnings = app.warning.getvalue().split('\n')
493-
assert len(warnings) == 2
494-
assert warnings[-1] == ''
495-
assert (
496-
'Dup2.rst:4: WARNING: duplicate token description of Dup, other instance in Dup1'
497-
in warnings[0]
498-
)
499-
500-
etree = etree_parse(app.outdir / 'index.html')
501-
nodes = list(etree.iter('ul'))
502-
assert len(nodes) >= 3
503-
504-
ul = nodes[2]
505-
cases = []
506-
for li in list(ul):
507-
li_list = list(li)
508-
assert len(li_list) == 1
509-
p = li_list[0]
510-
assert p.tag == 'p'
511-
text = str(p.text).strip(' :')
512-
p_list = list(p)
513-
assert len(p_list) == 1
514-
a = p_list[0]
515-
assert a.tag == 'a'
516-
link = a.get('href')
517-
a_list = list(a)
518-
assert len(a_list) == 1
519-
code = a_list[0]
520-
assert code.tag == 'code'
521-
code_list = list(code)
522-
assert len(code_list) == 1
523-
span = code_list[0]
524-
assert span.tag == 'span'
525-
link_text = span.text.strip()
526-
cases.append((text, link, link_text))
527-
assert cases == [
528-
('A', 'Bare.html#grammar-token-A', 'A'),
529-
('B', 'Bare.html#grammar-token-B', 'B'),
530-
('P1:A', 'P1.html#grammar-token-P1-A', 'P1:A'),
531-
('P1:B', 'P1.html#grammar-token-P1-B', 'P1:B'),
532-
('P2:A', 'P1.html#grammar-token-P1-A', 'P1:A'),
533-
('P2:B', 'P2.html#grammar-token-P2-B', 'P2:B'),
534-
('Explicit title A, plain', 'Bare.html#grammar-token-A', 'MyTitle'),
535-
('Explicit title A, colon', 'Bare.html#grammar-token-A', 'My:Title'),
536-
('Explicit title P1:A, plain', 'P1.html#grammar-token-P1-A', 'MyTitle'),
537-
('Explicit title P1:A, colon', 'P1.html#grammar-token-P1-A', 'My:Title'),
538-
('Tilde A', 'Bare.html#grammar-token-A', 'A'),
539-
('Tilde P1:A', 'P1.html#grammar-token-P1-A', 'A'),
540-
('Tilde explicit title P1:A', 'P1.html#grammar-token-P1-A', '~MyTitle'),
541-
('Tilde, explicit title P1:A', 'P1.html#grammar-token-P1-A', 'MyTitle'),
542-
('Dup', 'Dup2.html#grammar-token-Dup', 'Dup'),
543-
('FirstLine', 'firstLineRule.html#grammar-token-FirstLine', 'FirstLine'),
544-
('SecondLine', 'firstLineRule.html#grammar-token-SecondLine', 'SecondLine'),
545-
]
546-
547-
text = (app.outdir / 'LineContinuation.html').read_text(encoding='utf8')
548-
assert 'A</strong> ::= B C D E F G' in text
549-
550-
551-
@pytest.mark.sphinx('html', testroot='root')
552-
def test_productionlist2(app):
553-
text = '.. productionlist:: P2\n A: `:A` `A`\n B: `P1:B` `~P1:B`\n'
554-
doctree = restructuredtext.parse(app, text)
555-
refnodes = list(doctree.findall(pending_xref))
556-
assert_node(refnodes[0], pending_xref, reftarget='A')
557-
assert_node(refnodes[1], pending_xref, reftarget='P2:A')
558-
assert_node(refnodes[2], pending_xref, reftarget='P1:B')
559-
assert_node(refnodes[3], pending_xref, reftarget='P1:B')
560-
assert_node(refnodes[0], [pending_xref, nodes.literal, 'A'])
561-
assert_node(refnodes[1], [pending_xref, nodes.literal, 'A'])
562-
assert_node(refnodes[2], [pending_xref, nodes.literal, 'P1:B'])
563-
assert_node(refnodes[3], [pending_xref, nodes.literal, 'B'])
564-
565-
566488
@pytest.mark.sphinx('html', testroot='root')
567489
def test_disabled_docref(app):
568490
text = ':doc:`index`\n:doc:`!index`\n'

0 commit comments

Comments
 (0)