1313from sphinx .util .nodes import make_id
1414
1515if TYPE_CHECKING :
16- from collections .abc import Sequence
16+ from collections .abc import Iterable , Iterator , Sequence
1717 from typing import Any , Final
1818
1919 from docutils .nodes import Node
@@ -62,28 +62,15 @@ def make_grammar_snippet(
6262
6363 group_name = options ['group' ]
6464 node_location = self .get_location ()
65- current_lines = []
66- production_node = addnodes .production ()
67- production_nodes = [production_node ]
68- for source_line in content :
69- # Start a new production_node if there's text in the first
70- # column of the line. (That means a new rule is starting.)
71- if not source_line [:1 ].isspace ():
72- # set the raw source of the previous production
73- production_node .rawsource = '\n ' .join (current_lines )
74- current_lines .clear ()
75- # start a new production_node
76- production_node = addnodes .production ()
77- production_nodes .append (production_node )
78- current_lines .append (source_line )
79- self .add_production_line (
80- production_node ,
81- source_line ,
65+ production_nodes = []
66+ for rawsource , production_defs in self .production_definitions (content ):
67+ production = self .make_production (
68+ rawsource ,
69+ production_defs ,
8270 group_name = group_name ,
8371 location = node_location ,
8472 )
85- # set the raw source of the final production
86- production_node .rawsource = '\n ' .join (current_lines )
73+ production_nodes .append (production )
8774
8875 node = addnodes .productionlist (
8976 '' ,
@@ -94,29 +81,59 @@ def make_grammar_snippet(
9481 self .set_source_info (node )
9582 return [node ]
9683
97- def add_production_line (
84+ def production_definitions (
85+ self , lines : Iterable [str ], /
86+ ) -> Iterator [tuple [str , list [dict [str , str ]]]]:
87+ """Yield pairs of rawsource and production content dicts."""
88+ production_lines : list [str ] = []
89+ production_content : list [dict [str , str ]] = []
90+ for line in lines :
91+ # If this line is the start of a new rule (text in the column 1),
92+ # emit the current production and start a new one.
93+ if not line [:1 ].isspace ():
94+ rawsource = '\n ' .join (production_lines )
95+ production_lines .clear ()
96+ if production_content :
97+ yield rawsource , production_content
98+ production_content = []
99+
100+ # Append the current line for the raw source
101+ production_lines .append (line )
102+
103+ # Parse the line into constituent parts
104+ last_pos = 0
105+ for match in self .grammar_re .finditer (line ):
106+ # Handle text between matches
107+ if match .start () > last_pos :
108+ unmatched_text = line [last_pos : match .start ()]
109+ production_content .append ({'text' : unmatched_text })
110+ last_pos = match .end ()
111+
112+ # Handle matches
113+ group_dict = {
114+ name : content
115+ for name , content in match .groupdict ().items ()
116+ if content is not None
117+ }
118+ production_content .append (group_dict )
119+ production_content .append ({'text' : line [last_pos :] + '\n ' })
120+
121+ # Emit the final production
122+ if production_content :
123+ rawsource = '\n ' .join (production_lines )
124+ yield rawsource , production_content
125+
126+ def make_production (
98127 self ,
99- production_node : addnodes . production ,
100- source_line : str ,
128+ rawsource : str ,
129+ production_defs : list [ dict [ str , str ]] ,
101130 * ,
102131 group_name : str ,
103132 location : str ,
104- ) -> None :
105- """Add one line of content to the given production_node"""
106- last_pos = 0
107- for match in self .grammar_re .finditer (source_line ):
108- # Handle text between matches
109- if match .start () > last_pos :
110- unmatched_text = source_line [last_pos : match .start ()]
111- production_node += nodes .Text (unmatched_text )
112- last_pos = match .end ()
113-
114- # Handle matches
115- group_dict = {
116- name : content
117- for name , content in match .groupdict ().items ()
118- if content is not None
119- }
133+ ) -> addnodes .production :
134+ """Create a production node from a list of parts."""
135+ production_node = addnodes .production (rawsource )
136+ for group_dict in production_defs :
120137 match group_dict :
121138 case {'rule_name' : name }:
122139 production_node += self .make_name_target (
@@ -128,9 +145,11 @@ def add_production_line(
128145 production_node += token_xrefs (ref_text , group_name )
129146 case {'single_quoted' : name } | {'double_quoted' : name }:
130147 production_node += snippet_string_node ('' , name )
148+ case {'text' : text }:
149+ production_node += nodes .Text (text )
131150 case _:
132151 raise ValueError ('unhandled match' )
133- production_node += nodes . Text ( source_line [ last_pos :] + ' \n ' )
152+ return production_node
134153
135154 def make_name_target (
136155 self ,
0 commit comments