Skip to content

Commit eed2524

Browse files
committed
Remove productionlist hard-coding in translators
1 parent a953490 commit eed2524

File tree

8 files changed

+146
-114
lines changed

8 files changed

+146
-114
lines changed

doc/usage/restructuredtext/directives.rst

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,7 +1647,7 @@ derived forms), but provides enough to allow context-free grammars to be
16471647
displayed in a way that causes uses of a symbol to be rendered as hyperlinks to
16481648
the definition of the symbol. There is this directive:
16491649

1650-
.. rst:directive:: .. productionlist:: [productionGroup]
1650+
.. rst:directive:: .. productionlist:: [production_group]
16511651
16521652
This directive is used to enclose a group of productions. Each production
16531653
is given on a single line and consists of a name, separated by a colon from
@@ -1662,26 +1662,26 @@ the definition of the symbol. There is this directive:
16621662
production list, you can reference to token productions using
16631663
:rst:role:`token`.
16641664

1665-
The *productionGroup* argument to :rst:dir:`productionlist` serves to
1665+
The *production_group* argument to :rst:dir:`productionlist` serves to
16661666
distinguish different sets of production lists that belong to different
1667-
grammars. Multiple production lists with the same *productionGroup* thus
1667+
grammars. Multiple production lists with the same *production_group* thus
16681668
define rules in the same scope.
16691669

16701670
Inside of the production list, tokens implicitly refer to productions
16711671
from the current group. You can refer to the production of another
16721672
grammar by prefixing the token with its group name and a colon, e.g,
1673-
"``otherGroup:sum``". If the group of the token should not be shown in
1673+
"``other-group:sum``". If the group of the token should not be shown in
16741674
the production, it can be prefixed by a tilde, e.g.,
1675-
"``~otherGroup:sum``". To refer to a production from an unnamed
1675+
"``~other-group:sum``". To refer to a production from an unnamed
16761676
grammar, the token should be prefixed by a colon, e.g., "``:sum``".
16771677

16781678
Outside of the production list,
1679-
if you have given a *productionGroup* argument you must prefix the
1679+
if you have given a *production_group* argument you must prefix the
16801680
token name in the cross-reference with the group name and a colon,
1681-
e.g., "``myGroup:sum``" instead of just "``sum``".
1681+
e.g., "``my_group:sum``" instead of just "``sum``".
16821682
If the group should not be shown in the title of the link either
1683-
an explicit title can be given (e.g., "``myTitle <myGroup:sum>``"),
1684-
or the target can be prefixed with a tilde (e.g., "``~myGroup:sum``").
1683+
an explicit title can be given (e.g., "``myTitle <my_group:sum>``"),
1684+
or the target can be prefixed with a tilde (e.g., "``~my_group:sum``").
16851685

16861686
Note that no further reStructuredText parsing is done in the production,
16871687
so that you don't have to escape ``*`` or ``|`` characters.

sphinx/domains/std/__init__.py

Lines changed: 80 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from sphinx.util.parsing import nested_parse_to_nodes
2323

2424
if TYPE_CHECKING:
25-
from collections.abc import Callable, Iterable, Iterator, Set
25+
from collections.abc import Callable, Iterable, Iterator, Sequence, Set
2626
from typing import Any, ClassVar, Final
2727

2828
from docutils.nodes import Element, Node, system_message
@@ -597,41 +597,91 @@ class ProductionList(SphinxDirective):
597597
option_spec: ClassVar[OptionSpec] = {}
598598

599599
def run(self) -> list[Node]:
600-
domain = self.env.domains.standard_domain
601-
node: Element = addnodes.productionlist()
600+
node = addnodes.productionlist()
602601
self.set_source_info(node)
603602
# The backslash handling is from ObjectDescription.get_signatures
604603
nl_escape_re = re.compile(r'\\\n')
605604
lines = nl_escape_re.sub('', self.arguments[0]).split('\n')
605+
production_group = self.production_group(lines, self.options)
606+
production_lines = list(self.production_definitions(lines))
607+
max_len = max(len(name) for _, name, _ in production_lines)
608+
node_location = self.get_location()
609+
node += [
610+
self.make_production(
611+
rawsource=rule,
612+
name=name,
613+
tokens=tokens,
614+
production_group=production_group,
615+
max_len=max_len,
616+
location=node_location,
617+
)
618+
for rule, name, tokens in production_lines
619+
]
620+
return [node]
606621

607-
production_group = ''
608-
first_rule_seen = False
609-
for rule in lines:
610-
if not first_rule_seen and ':' not in rule:
611-
production_group = rule.strip()
612-
continue
613-
first_rule_seen = True
614-
try:
615-
name, tokens = rule.split(':', 1)
616-
except ValueError:
622+
@staticmethod
623+
def production_group(lines: Sequence[str], options: dict[str, Any]) -> str: # NoQA: ARG004
624+
# get production_group
625+
if not lines or ':' in lines[0]:
626+
return ''
627+
production_group = lines[0].strip()
628+
lines[:] = lines[1:]
629+
return production_group
630+
631+
@staticmethod
632+
def production_definitions(lines: Iterable[str]) -> Iterator[tuple[str, str, str]]:
633+
"""Yield triples of rawsource, name, definition."""
634+
for line in lines:
635+
if ':' not in line:
617636
break
618-
subnode = addnodes.production(rule)
619-
name = name.strip()
620-
subnode['tokenname'] = name
621-
if subnode['tokenname']:
622-
prefix = 'grammar-token-%s' % production_group
623-
node_id = make_id(self.env, self.state.document, prefix, name)
624-
subnode['ids'].append(node_id)
625-
self.state.document.note_implicit_target(subnode, subnode)
626-
627-
if len(production_group) != 0:
628-
obj_name = f'{production_group}:{name}'
629-
else:
630-
obj_name = name
631-
domain.note_object('token', obj_name, node_id, location=node)
632-
subnode.extend(token_xrefs(tokens, production_group=production_group))
633-
node.append(subnode)
634-
return [node]
637+
name, _, tokens = line.partition(':')
638+
yield line, name.strip(), tokens.strip()
639+
640+
def make_production(
641+
self,
642+
rawsource: str,
643+
name: str,
644+
tokens: str,
645+
production_group: str,
646+
max_len: int,
647+
location: str,
648+
) -> addnodes.production:
649+
production_node = addnodes.production(rawsource, tokenname=name)
650+
if name:
651+
production_node += self.make_target(name, production_group, location)
652+
else:
653+
production_node += self.continuation_padding(max_len)
654+
production_node.append(self.production_separator(name, max_len))
655+
production_node += token_xrefs(tokens, production_group=production_group)
656+
production_node.append(nodes.Text('\n'))
657+
return production_node
658+
659+
def make_target(
660+
self,
661+
name: str,
662+
production_group: str,
663+
location: str,
664+
) -> addnodes.literal_strong:
665+
"""Make a link target for the given production."""
666+
name_node = addnodes.literal_strong(name, name)
667+
prefix = f'grammar-token-{production_group}'
668+
node_id = make_id(self.env, self.state.document, prefix, name)
669+
name_node['ids'].append(node_id)
670+
self.state.document.note_implicit_target(name_node, name_node)
671+
obj_name = f'{production_group}:{name}' if production_group else name
672+
std = self.env.domains.standard_domain
673+
std.note_object('token', obj_name, node_id, location=location)
674+
return name_node
675+
676+
@staticmethod
677+
def continuation_padding(max_len: int) -> nodes.Text:
678+
return nodes.Text(' ' * max_len)
679+
680+
@staticmethod
681+
def production_separator(name: str, max_len: int) -> nodes.Text:
682+
if name:
683+
return nodes.Text(' ::= '.rjust(max_len - len(name) + 5))
684+
return nodes.Text(' ')
635685

636686

637687
class TokenXRefRole(XRefRole):

sphinx/writers/html5.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import posixpath
66
import re
77
import urllib.parse
8-
from typing import TYPE_CHECKING, cast
8+
from typing import TYPE_CHECKING
99

1010
from docutils import nodes
1111
from docutils.writers.html5_polyglot import HTMLTranslator as BaseTranslator
@@ -17,8 +17,6 @@
1717
from sphinx.util.images import get_image_size
1818

1919
if TYPE_CHECKING:
20-
from collections.abc import Iterable
21-
2220
from docutils.nodes import Element, Node, Text
2321

2422
from sphinx.builders import Builder
@@ -695,23 +693,9 @@ def depart_literal(self, node: Element) -> None:
695693

696694
def visit_productionlist(self, node: Element) -> None:
697695
self.body.append(self.starttag(node, 'pre'))
698-
productionlist = cast('Iterable[addnodes.production]', node)
699-
maxlen = max(len(production['tokenname']) for production in productionlist)
700-
lastname = None
701-
for production in productionlist:
702-
if production['tokenname']:
703-
lastname = production['tokenname'].ljust(maxlen)
704-
self.body.append(self.starttag(production, 'strong', ''))
705-
self.body.append(lastname + '</strong> ::= ')
706-
elif lastname is not None:
707-
self.body.append(' ' * (maxlen + 5))
708-
production.walkabout(self)
709-
self.body.append('\n')
710-
self.body.append('</pre>\n')
711-
raise nodes.SkipNode
712696

713697
def depart_productionlist(self, node: Element) -> None:
714-
pass
698+
self.body.append('</pre>\n')
715699

716700
def visit_production(self, node: Element) -> None:
717701
pass

sphinx/writers/latex.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ def __init__(
323323

324324
# flags
325325
self.in_title = 0
326-
self.in_production_list = 0
326+
self.in_production_list = False
327327
self.in_footnote = 0
328328
self.in_caption = 0
329329
self.in_term = 0
@@ -671,20 +671,25 @@ def depart_glossary(self, node: Element) -> None:
671671
def visit_productionlist(self, node: Element) -> None:
672672
self.body.append(BLANKLINE)
673673
self.body.append(r'\begin{productionlist}' + CR)
674-
self.in_production_list = 1
674+
self.in_production_list = True
675675

676676
def depart_productionlist(self, node: Element) -> None:
677677
self.body.append(r'\end{productionlist}' + BLANKLINE)
678-
self.in_production_list = 0
678+
self.in_production_list = False
679679

680680
def visit_production(self, node: Element) -> None:
681681
if node['tokenname']:
682682
tn = node['tokenname']
683-
self.body.append(self.hypertarget('grammar-token-' + tn))
683+
self.body.append(self.hypertarget(f'grammar-token-{tn}'))
684684
self.body.append(r'\production{%s}{' % self.encode(tn))
685685
else:
686686
self.body.append(r'\productioncont{')
687687

688+
# remove name/padding and seperator child nodes,
689+
# these are handled by '\production' and '\productioncont'
690+
# TODO: remove special LaTeX handling of production nodes
691+
node[:] = node[2:]
692+
688693
def depart_production(self, node: Element) -> None:
689694
self.body.append('}' + CR)
690695

@@ -2070,9 +2075,13 @@ def depart_strong(self, node: Element) -> None:
20702075
self.body.append('}')
20712076

20722077
def visit_literal_strong(self, node: Element) -> None:
2078+
if self.in_production_list:
2079+
return
20732080
self.body.append(r'\sphinxstyleliteralstrong{\sphinxupquote{')
20742081

20752082
def depart_literal_strong(self, node: Element) -> None:
2083+
if self.in_production_list:
2084+
return
20762085
self.body.append('}}')
20772086

20782087
def visit_abbreviation(self, node: Element) -> None:

sphinx/writers/manpage.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@ class ManualPageTranslator(SphinxTranslator, BaseTranslator): # type: ignore[mi
7979
def __init__(self, document: nodes.document, builder: Builder) -> None:
8080
super().__init__(document, builder)
8181

82-
self.in_productionlist = 0
83-
8482
# first title is the manpage title
8583
self.section_level = -1
8684

@@ -274,25 +272,10 @@ def depart_seealso(self, node: Element) -> None:
274272

275273
def visit_productionlist(self, node: Element) -> None:
276274
self.ensure_eol()
277-
self.in_productionlist += 1
278275
self.body.append('.sp\n.nf\n')
279-
productionlist = cast('Iterable[addnodes.production]', node)
280-
maxlen = max(len(production['tokenname']) for production in productionlist)
281-
lastname = None
282-
for production in productionlist:
283-
if production['tokenname']:
284-
lastname = production['tokenname'].ljust(maxlen)
285-
self.body.append(self.defs['strong'][0])
286-
self.body.append(self.deunicode(lastname))
287-
self.body.append(self.defs['strong'][1])
288-
self.body.append(' ::= ')
289-
elif lastname is not None:
290-
self.body.append(' ' * (maxlen + 5))
291-
production.walkabout(self)
292-
self.body.append('\n')
276+
277+
def depart_productionlist(self, node: Element) -> None:
293278
self.body.append('\n.fi\n')
294-
self.in_productionlist -= 1
295-
raise nodes.SkipNode
296279

297280
def visit_production(self, node: Element) -> None:
298281
pass

sphinx/writers/texinfo.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ def __init__(self, document: nodes.document, builder: TexinfoBuilder) -> None:
189189
self.escape_hyphens = 0
190190
self.curfilestack: list[str] = []
191191
self.footnotestack: list[dict[str, list[collected_footnote | bool]]] = []
192+
self.in_production_list = False
192193
self.in_footnote = 0
193194
self.in_samp = 0
194195
self.handled_abbrs: set[str] = set()
@@ -1308,20 +1309,11 @@ def unknown_departure(self, node: Node) -> None:
13081309

13091310
def visit_productionlist(self, node: Element) -> None:
13101311
self.visit_literal_block(None)
1311-
productionlist = cast('Iterable[addnodes.production]', node)
1312-
maxlen = max(len(production['tokenname']) for production in productionlist)
1313-
1314-
for production in productionlist:
1315-
if production['tokenname']:
1316-
for id in production.get('ids'):
1317-
self.add_anchor(id, production)
1318-
s = production['tokenname'].ljust(maxlen) + ' ::='
1319-
else:
1320-
s = ' ' * (maxlen + 4)
1321-
self.body.append(self.escape(s))
1322-
self.body.append(self.escape(production.astext() + '\n'))
1312+
self.in_production_list = True
1313+
1314+
def depart_productionlist(self, node: Element) -> None:
1315+
self.in_production_list = False
13231316
self.depart_literal_block(None)
1324-
raise nodes.SkipNode
13251317

13261318
def visit_production(self, node: Element) -> None:
13271319
pass
@@ -1336,9 +1328,15 @@ def depart_literal_emphasis(self, node: Element) -> None:
13361328
self.body.append('}')
13371329

13381330
def visit_literal_strong(self, node: Element) -> None:
1331+
if self.in_production_list:
1332+
for id_ in node['ids']:
1333+
self.add_anchor(id_, node)
1334+
return
13391335
self.body.append('@code{')
13401336

13411337
def depart_literal_strong(self, node: Element) -> None:
1338+
if self.in_production_list:
1339+
return
13421340
self.body.append('}')
13431341

13441342
def visit_index(self, node: Element) -> None:

0 commit comments

Comments
 (0)