Skip to content

Commit

Permalink
Remove monkey patch for production list and Add CompatProductionList
Browse files Browse the repository at this point in the history
to conserve the productionlist usage in the docs.
  • Loading branch information
blaisep committed Dec 4, 2024
1 parent f6ffb21 commit 5cfc701
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 97 deletions.
2 changes: 1 addition & 1 deletion Doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@
'availability',
'c_annotations',
'glossary_search',
'grammar_snippet',
'lexers',
'pyspecific',
'sphinx.ext.coverage',
'sphinx.ext.doctest',
'sphinx.ext.extlinks',
'grammar_snippet',
]

# Skip if downstream redistributors haven't installed them
Expand Down
198 changes: 108 additions & 90 deletions Doc/tools/extensions/grammar_snippet.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,98 @@
from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import make_id

def make_snippet(directive, options, content):

Check failure on line 9 in Doc/tools/extensions/grammar_snippet.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

Doc/tools/extensions/grammar_snippet.py:1:1: I001 Import block is un-sorted or un-formatted

Check failure on line 9 in Doc/tools/extensions/grammar_snippet.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (E302)

Doc/tools/extensions/grammar_snippet.py:9:1: E302 Expected 2 blank lines, found 1
group_name = options['group']

# Docutils elements have a `rawsource` attribute that is supposed to be
# set to the original ReST source.
# Sphinx does the following with it:
# - if it's empty, set it to `self.astext()`
# - if it matches `self.astext()` when generating the output,
# apply syntax highlighting (which is based on the plain-text content
# and thus discards internal formatting, like references).
# To get around this, we set it to this non-empty string:
rawsource = 'You should not see this.'

literal = nodes.literal_block(
rawsource,
'',
# TODO: Use a dedicated CSS class here and for strings.
# and add it to the theme too
classes=['highlight'],
)

grammar_re = re.compile(
"""
(?P<rule_name>^[a-zA-Z0-9_]+) # identifier at start of line
(?=:) # ... followed by a colon
|
[`](?P<rule_ref>[a-zA-Z0-9_]+)[`] # identifier in backquotes
|
(?P<single_quoted>'[^']*') # string in 'quotes'
|
(?P<double_quoted>"[^"]*") # string in "quotes"
""",
re.VERBOSE,
)

for line in content:
last_pos = 0
for match in grammar_re.finditer(line):
# Handle text between matches
if match.start() > last_pos:
literal += nodes.Text(line[last_pos:match.start()])
last_pos = match.end()

# Handle matches
groupdict = {
name: content
for name, content in match.groupdict().items()
if content is not None
}
match groupdict:
case {'rule_name': name}:
name_node = addnodes.literal_strong()

# Cargo-culted magic to make `name_node` a link target
# similar to Sphinx `production`.
# This needs to be the same as what Sphinx does
# to avoid breaking existing links.
domain = directive.env.domains['std']
obj_name = f"{group_name}:{name}"
prefix = f'grammar-token-{group_name}'
node_id = make_id(directive.env, directive.state.document, prefix, name)
name_node['ids'].append(node_id)
directive.state.document.note_implicit_target(name_node, name_node)
domain.note_object('token', obj_name, node_id, location=name_node)

text_node = nodes.Text(name)
name_node += text_node
literal += name_node
case {'rule_ref': name}:
ref_node = addnodes.pending_xref(
name,
reftype="token",
refdomain="std",
reftarget=f"{group_name}:{name}",
)
ref_node += nodes.Text(name)
literal += ref_node
case {'single_quoted': name} | {'double_quoted': name}:
string_node = nodes.inline(classes=['nb'])
string_node += nodes.Text(name)
literal += string_node
case _:
raise ValueError('unhandled match')
literal += nodes.Text(line[last_pos:] + '\n')

node = nodes.paragraph(
'', '',
literal,
)

return [node]


class GrammarSnippetDirective(SphinxDirective):
"""Transform a grammar-snippet directive to a Sphinx productionlist
Expand Down Expand Up @@ -37,97 +129,23 @@ class GrammarSnippetDirective(SphinxDirective):
final_argument_whitespace = True

def run(self):
group_name = self.options['group']

# Docutils elements have a `rawsource` attribute that is supposed to be
# set to the original ReST source.
# Sphinx does the following with it:
# - if it's empty, set it to `self.astext()`
# - if it matches `self.astext()` when generating the output,
# apply syntax highlighting (which is based on the plain-text content
# and thus discards internal formatting, like references).
# To get around this, we set it to this non-empty string:
rawsource = 'You should not see this.'

literal = nodes.literal_block(
rawsource,
'',
# TODO: Use a dedicated CSS class here and for strings.
# and add it to the theme too
classes=['highlight'],
)

grammar_re = re.compile(
"""
(?P<rule_name>^[a-zA-Z0-9_]+) # identifier at start of line
(?=:) # ... followed by a colon
|
[`](?P<rule_ref>[a-zA-Z0-9_]+)[`] # identifier in backquotes
|
(?P<single_quoted>'[^']*') # string in 'quotes'
|
(?P<double_quoted>"[^"]*") # string in "quotes"
""",
re.VERBOSE,
)

for line in self.content:
last_pos = 0
for match in grammar_re.finditer(line):
# Handle text between matches
if match.start() > last_pos:
literal += nodes.Text(line[last_pos:match.start()])
last_pos = match.end()

# Handle matches
groupdict = {
name: content
for name, content in match.groupdict().items()
if content is not None
}
match groupdict:
case {'rule_name': name}:
name_node = addnodes.literal_strong()

# Cargo-culted magic to make `name_node` a link target
# similar to Sphinx `production`.
# This needs to be the same as what Sphinx does
# to avoid breaking existing links.
domain = self.env.domains['std']
obj_name = f"{group_name}:{name}"
prefix = f'grammar-token-{group_name}'
node_id = make_id(self.env, self.state.document, prefix, name)
name_node['ids'].append(node_id)
self.state.document.note_implicit_target(name_node, name_node)
domain.note_object('token', obj_name, node_id, location=name_node)

text_node = nodes.Text(name)
name_node += text_node
literal += name_node
case {'rule_ref': name}:
ref_node = addnodes.pending_xref(
name,
reftype="token",
refdomain="std",
reftarget=f"{group_name}:{name}",
)
ref_node += nodes.Text(name)
literal += ref_node
case {'single_quoted': name} | {'double_quoted': name}:
string_node = nodes.inline(classes=['nb'])
string_node += nodes.Text(name)
literal += string_node
case _:
raise ValueError('unhandled match')
literal += nodes.Text(line[last_pos:] + '\n')

node = nodes.paragraph(
'', '',
literal,
)

return [node]
return make_snippet(self, self.options, self.content)


class CompatProductionList(SphinxDirective):
has_content = True
option_spec = {}

# We currently ignore arguments.
required_arguments = 1

def run(self):
options = {'group': self.arguments[0]}
content = self.content
return make_snippet(self, options, content)


def setup(app):
app.add_directive('grammar-snippet', GrammarSnippetDirective)
app.add_directive('productionlist', CompatProductionList, override=True)
return {'version': '1.0', 'parallel_read_safe': True}
6 changes: 0 additions & 6 deletions Doc/tools/extensions/pyspecific.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,6 @@
Body.enum.converters['lowerroman'] = \
Body.enum.converters['upperroman'] = lambda x: None

# monkey-patch the productionlist directive to allow hyphens in group names
# https://github.com/sphinx-doc/sphinx/issues/11854
from sphinx.domains import std

std.token_re = re.compile(r'`((~?[\w-]*:)?\w+)`')

# backport :no-index:
PyModule.option_spec['no-index'] = directives.flag

Expand Down

0 comments on commit 5cfc701

Please sign in to comment.