@@ -54,6 +54,11 @@ def __init__(
5454 self .BNGPATH = BNGPATH
5555 self .bngexec = bngexec
5656 self .parsed_actions = []
57+ # Actions that live inside a ``begin protocol``/``end protocol``
58+ # block, stored separately from top-level actions so BNGParser can
59+ # round-trip them into a ProtocolBlock instead of folding them into
60+ # the top-level ActionBlock.
61+ self .parsed_protocol_actions = []
5762
5863 def generate_xml (self , xml_file , model_file = None ) -> bool :
5964 """
@@ -150,17 +155,51 @@ def strip_actions(self, model_path, folder) -> str:
150155 with open (model_path , "r" , encoding = "UTF-8" ) as mf :
151156 # read and strip actions
152157 mstr = mf .read ()
153- # TODO: Clean this up _a lot_
154- # this removes any new line escapes (\ \n) to continue
155- # to another line, so we can just remove the action lines
156- mstr = re .sub (r"\\\n" , "" , mstr )
158+ # Collapse `\<newline>` line continuations before stripping
159+ # action lines, so the action parser sees the same logical
160+ # command boundaries as BNG.
161+ #
162+ # Only collapse `\` that appears before any `#` on its line.
163+ # A continuation marker after the comment introducer is part
164+ # of the comment body in BNG2.pl — collapsing it would glue
165+ # the next physical line (often a real definition) into the
166+ # comment, dropping it from the model. Repro: a commented-out
167+ # `# foo()=if(t<42,0,\` immediately above a live
168+ # `foo()=if(t<42,9.899,\` definition would silently lose the
169+ # live function from the regenerated `.bngl` (and from any
170+ # `.net` BNG2.pl generated downstream).
171+ mstr = re .sub (r"^([^#\n]*)\\\n" , r"\1" , mstr , flags = re .MULTILINE )
157172 mlines = mstr .split ("\n " )
158- stripped_lines = list (filter (lambda x : self ._not_action (x ), mlines ))
159- # remove spaces, actions don't allow them
160- self .parsed_actions = [
161- x .replace (" " , "" )
162- for x in filter (lambda x : not self ._not_action (x ), mlines )
163- ]
173+ # Walk the lines once, separating non-action content (kept in the
174+ # stripped output for BNG2.pl) from action-shaped lines, and
175+ # further splitting action-shaped lines based on whether they sit
176+ # inside a ``begin protocol``/``end protocol`` block. Protocol
177+ # actions are tracked separately so BNGParser can round-trip them
178+ # into a ProtocolBlock instead of the top-level ActionBlock.
179+ self .parsed_actions = []
180+ self .parsed_protocol_actions = []
181+ stripped_lines = []
182+ in_protocol = False
183+ for line in mlines :
184+ if re .match (r"\s*(begin)\s+(protocol)\b" , line ):
185+ in_protocol = True
186+ stripped_lines .append (line )
187+ continue
188+ if re .match (r"\s*(end)\s+(protocol)\b" , line ):
189+ in_protocol = False
190+ stripped_lines .append (line )
191+ continue
192+ if self ._not_action (line ):
193+ stripped_lines .append (line )
194+ continue
195+ # Hand the action line off to BNGParser intact — quoted
196+ # spans (e.g. ``param=>"-v -gml 1000000"``) need to survive
197+ # the whitespace-collapse pass, which `_normalize_action_text`
198+ # does in a quote-aware way.
199+ if in_protocol :
200+ self .parsed_protocol_actions .append (line )
201+ else :
202+ self .parsed_actions .append (line )
164203 # let's remove begin/end actions, rarely used but should be removed
165204 remove_from = - 1
166205 remove_to = - 1
@@ -192,8 +231,14 @@ def strip_actions(self, model_path, folder) -> str:
192231 return stripped_model
193232
194233 def _not_action (self , line ) -> bool :
234+ # Anchor the match to the start of the (left-stripped) line so that
235+ # user identifiers containing an action name as a substring — most
236+ # commonly ``conversion()`` (the substring ``version(`` matches the
237+ # ``version`` action) inside a ``begin functions`` block — aren't
238+ # misclassified and pulled out as actions.
239+ stripped = line .lstrip ()
195240 for action in self ._action_list :
196- if action in line :
241+ if stripped . startswith ( action ) :
197242 return False
198243 return True
199244
0 commit comments