Skip to content

Commit 86a0ecc

Browse files
encukoublaisepwqferr
committed
First version of GrammarSnippetDirective, copied from an earlier branch
Co-authored-by: Blaise Pabon <[email protected]> Co-authored-by: William Ferreira <[email protected]>
1 parent 9d2a879 commit 86a0ecc

File tree

1 file changed

+134
-0
lines changed

1 file changed

+134
-0
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
2+
class GrammarSnippetDirective(SphinxDirective):
3+
"""Transform a grammar-snippet directive to a Sphinx productionlist
4+
5+
That is, turn something like:
6+
7+
.. grammar-snippet:: file
8+
:group: python-grammar
9+
:generated-by: Tools/peg_generator/docs_generator.py
10+
11+
file: (NEWLINE | statement)*
12+
13+
into something like:
14+
15+
.. productionlist:: python-grammar
16+
file: (NEWLINE | statement)*
17+
18+
The custom directive is needed because Sphinx's `productionlist` does
19+
not support options.
20+
"""
21+
has_content = True
22+
option_spec = {
23+
'group': directives.unchanged,
24+
'generated-by': directives.unchanged,
25+
'diagrams': directives.unchanged,
26+
}
27+
28+
# Arguments are used by the tool that generates grammar-snippet,
29+
# this Directive ignores them.
30+
required_arguments = 1
31+
optional_arguments = 0
32+
final_argument_whitespace = True
33+
34+
def run(self):
35+
group_name = self.options['group']
36+
37+
rawsource = '''
38+
# Docutils elements have a `rawsource` attribute that is supposed to be
39+
# set to the original ReST source.
40+
# Sphinx does the following with it:
41+
# - if it's empty, set it to `self.astext()`
42+
# - if it matches `self.astext()` when generating the output,
43+
# apply syntax highlighting (which is based on the plain-text content
44+
# and thus discards internal formatting, like references).
45+
# To get around this, we set it to this fake (and very non-empty)
46+
# string!
47+
'''
48+
49+
literal = nodes.literal_block(
50+
rawsource,
51+
'',
52+
# TODO: Use a dedicated CSS class here and for strings,
53+
# and add it to the theme too
54+
classes=['highlight'],
55+
)
56+
57+
grammar_re = re.compile(
58+
"""
59+
(?P<rule_name>^[a-zA-Z0-9_]+) # identifier at start of line
60+
(?=:) # ... followed by a colon
61+
|
62+
[`](?P<rule_ref>[a-zA-Z0-9_]+)[`] # identifier in backquotes
63+
|
64+
(?P<single_quoted>'[^']*') # string in 'quotes'
65+
|
66+
(?P<double_quoted>"[^"]*") # string in "quotes"
67+
""",
68+
re.VERBOSE,
69+
)
70+
71+
for line in self.content:
72+
last_pos = 0
73+
for match in grammar_re.finditer(line):
74+
# Handle text between matches
75+
if match.start() > last_pos:
76+
literal += nodes.Text(line[last_pos:match.start()])
77+
last_pos = match.end()
78+
79+
# Handle matches
80+
groupdict = {
81+
name: content
82+
for name, content in match.groupdict().items()
83+
if content is not None
84+
}
85+
match groupdict:
86+
case {'rule_name': name}:
87+
name_node = addnodes.literal_strong()
88+
89+
# Cargo-culted magic to make `name_node` a link target
90+
# similar to Sphinx `production`:
91+
domain = self.env.domains['std']
92+
obj_name = f"{group_name}:{name}"
93+
prefix = f'grammar-token-{group_name}'
94+
node_id = make_id(self.env, self.state.document, prefix, name)
95+
name_node['ids'].append(node_id)
96+
self.state.document.note_implicit_target(name_node, name_node)
97+
domain.note_object('token', obj_name, node_id, location=name_node)
98+
99+
text_node = nodes.Text(name)
100+
name_node += text_node
101+
literal += name_node
102+
case {'rule_ref': name}:
103+
ref_node = addnodes.pending_xref(
104+
name,
105+
reftype="token",
106+
refdomain="std",
107+
reftarget=f"{group_name}:{name}",
108+
)
109+
ref_node += nodes.Text(name)
110+
literal += ref_node
111+
case {'single_quoted': name} | {'double_quoted': name}:
112+
string_node = nodes.inline(classes=['nb'])
113+
string_node += nodes.Text(name)
114+
literal += string_node
115+
case _:
116+
raise ValueError('unhandled match')
117+
literal += nodes.Text(line[last_pos:] + '\n')
118+
119+
120+
node = nodes.paragraph(
121+
'', '',
122+
literal,
123+
)
124+
125+
content = StringList()
126+
for rule_name in self.options['diagrams'].split():
127+
content.append('', source=__file__)
128+
content.append(f'``{rule_name}``:', source=__file__)
129+
content.append('', source=__file__)
130+
content.append(f'.. image:: diagrams/{rule_name}.svg', source=__file__)
131+
132+
self.state.nested_parse(content, 0, node)
133+
134+
return [node]

0 commit comments

Comments
 (0)